62 Commits
Author SHA1 Message Date
minimons f1dd5ed9b1 84: Validate 'jup' and the configured signer when constructing SolanaWalletImpl 2026-09-04 11:39:29 +02:00
minimons b6a6d544b5 83: Remove lifecycle operations from the EvelynIOUBurnerService API 2026-09-04 10:04:47 +02:00
minimons 681b6feef1 X: Remove old obsoleted TickerService 2026-09-04 09:45:33 +02:00
minimons d3315900b9 X: NFC - Refactoring and additional logging 2026-09-04 09:14:01 +02:00
minimons ddbf6f047d 82: Remove lifecycle operations from the TickerService API 2026-09-03 20:34:49 +02:00
minimons 461044a998 X: detag.conf moved to conf 2026-09-03 16:38:02 +02:00
minimons a62bd5df98 81: Control Nenjim process construction and ordered startup through NenjimHub.conf 2026-09-03 15:09:57 +02:00
minimons 5bff358d73 X: Git ignore update 2026-09-03 15:08:56 +02:00
minimons ed952fd6b1 80: Introduce an explicit Hub-managed JavaFX runtime service 2026-09-02 23:23:16 +02:00
minimons 0cb125ca9d 79: Integrate NenjimProcessManager with NenjimHub runtime status 2026-09-02 21:51:05 +02:00
minimons 7b1f6d18eb 78: Make JournalService a Hub-managed Nenjim process and integrate it into startup 2026-09-02 16:46:51 +02:00
minimons f5fc5bb3b5 77: Make NenjimHub the runtime owner and replace NenjimApplication with NenjimProcess 2026-09-02 15:59:56 +02:00
minimons a252c256c4 76: Introduce the Nenjim component registry and move runtime composition into its manager 2026-08-30 14:28:49 +02:00
minimons b230e15ece 75: Implement the Nenjim Journal model, text parser, manager and filesystem service 2026-08-20 12:28:16 +02:00
minimons f29013e9c2 X: Moved markdown files from Documentation to keep in sync 2026-08-16 12:37:04 +02:00
minimons c645ba34eb X: Add detag.conf.example file 2026-08-16 12:37:04 +02:00
minimons 7d1557932d X: Activating Burner service 2026-08-14 20:26:11 +02:00
minimons c70be47c27 X: Add private_key_clean.sh 2026-08-14 20:26:11 +02:00
minimons f5a2183a40 X: Clean-up and documentation updates 2026-08-14 13:50:10 +02:00
minimons 3d5de0e09b 72: Add the Evelyn IOU buyback-and-burn service 2026-08-13 16:47:01 +02:00
minimons 961f8c69cb 71: Implement Discord webhook notification delivery 2026-08-12 10:42:39 +02:00
minimons df504e29bf 70: Refactor the notification framework into transport-independent notification services 2026-08-11 20:03:06 +02:00
minimons b33db6da49 69: Add SPL-token burn support to SolanaWallet and SolanaBlockChain 2026-08-11 15:42:56 +02:00
minimons b86ec37bb1 68: Await CONFIRMED on-chain outcomes for Jupiter Swap and Perps operations 2026-08-11 12:31:22 +02:00
minimons cb2d3e2a16 67: Add generic awaitTransaction() support to SolanaBlockChain 2026-08-11 11:42:41 +02:00
minimons e7f551b8dd 51: Add exact-input Jupiter Swap V2 service 2026-08-11 07:40:15 +02:00
minimons 5a096379f2 X: SIMPLE ENABLE 2026-08-10 15:00:26 +02:00
minimons b429a61237 65: Persist Evelyn status-index history per named Evelyn instance 2026-08-08 23:53:06 +02:00
minimons 21bd57ae7a 64: Implement live Evelyn Price Index in Evelyn Mission Control 2026-08-08 21:50:02 +02:00
minimons 30cfcfc4ff X: DISABLE ALL SERVICES 2026-08-08 16:59:12 +02:00
minimons d5b733bc6f 63: Add Raydium Pool PriceSource with periodic polling 2026-08-08 16:10:28 +02:00
minimons cac555159d X: Rearrange source tree for misplaced services and plugins 2026-08-08 14:25:09 +02:00
minimons bc936e029d 62: Add AssetAZ Currency Identity Service and migrate CurrencyType 2026-08-08 00:40:24 +02:00
minimons ef1b5e0adc 61: Introduce PriceSource and PriceSink architecture for AssetAZ Ticker 2026-08-07 00:14:27 +02:00
minimons 0db74b3296 60: Add initial AssetAZ Ticker service 2026-08-06 17:34:49 +02:00
minimons d8dc494afa X: Statup checks for missing configs 2026-08-06 17:17:32 +02:00
minimons d6b765e9a5 X: Teach Codex how to read Gitea issues, understand ValueTags and Detagging 2026-08-05 16:56:30 +02:00
minimons fdefba91ad 59: Show Evelyn status indices in Mission Control Overview 2026-08-05 14:09:01 +02:00
minimons 493a6291a9 X: Add Cauldron explanation to openspec 2026-08-05 11:08:27 +02:00
minimons 6ad5ad0924 X: OpenSpec init 2026-08-05 10:56:51 +02:00
minimons d946508214 58: Add initial Suwimo Client bootstrap 2026-08-05 10:56:51 +02:00
minimons d9e0afeff9 57: Create an empty dummy Soda Task Manager bootstrap 2026-08-05 10:56:51 +02:00
minimons 3f5445e072 56: Add Nenjim Test Tool dummy to autostart 2026-08-05 10:56:51 +02:00
minimons 28844bb6db 55: Add TabPane to EMC 2026-08-05 10:56:51 +02:00
minimons 2177722fd9 54: Do not throw an exception is no alarms are configured 2026-08-05 10:12:39 +02:00
minimons da72aa4458 53: Create Nenjim Process Manager and some dummy services 2026-08-05 10:12:39 +02:00
minimons 70fbb793ec 52: Create embarrassing simple Evelyn Mission Control in JavaFX 2026-08-05 10:12:39 +02:00
minimons b9741d09fd 50: Add support for transferring SPL tokens 2026-08-05 10:12:39 +02:00
minimons 847c33000a 49: Complete wallet-bound Jupiter transaction flow 2026-08-05 10:12:39 +02:00
minimons a65a9ed043 49: Bind Jupiter position services to wallets 2026-08-05 10:12:39 +02:00
minimons 8189b16516 49: Use common wallet transaction flow for SOL transfers 2026-08-05 10:12:39 +02:00
minimons 8f6aac025c 49: Add wallet transaction submission operations 2026-08-05 10:12:39 +02:00
minimons 6c48e3741a 49: Hide low-level Jupiter position increase operations 2026-08-05 10:12:39 +02:00
minimons 2a64809d97 49: Migrate position decrease alarm to high-level operation 2026-08-05 10:12:39 +02:00
minimons b4b087cb3a 49: Add high-level Jupiter position decrease operation 2026-08-05 10:12:39 +02:00
minimons 4ffc9a75ae 49: Migrate position increase alarm to high-level operation 2026-08-05 10:12:39 +02:00
minimons bfda6f01b7 49: Add high-level Jupiter position increase operation 2026-08-05 10:12:39 +02:00
minimons 40ac60acd1 49: Remove low-level SOL builders from wallet API 2026-08-05 10:12:39 +02:00
minimons 1bde6c8152 49: Add high-level SOL send operations 2026-08-05 10:12:39 +02:00
minimons 1d63cdc026 49: Hide transaction handling behind high-level service operations 2026-08-05 10:12:39 +02:00
minimons 4b913d4c83 49: Hide transaction handling behind high-level service operations 2026-08-05 10:12:39 +02:00
minimons 280024c340 48: Add SOL transfer support to SolanaWallet 2026-08-05 10:12:39 +02:00
330 changed files with 23661 additions and 1239 deletions
@@ -0,0 +1,185 @@
---
name: openspec-apply-change
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Implement tasks from an OpenSpec change.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
Always announce: "Using change: <name>" and how to override (e.g., `$openspec-apply-change <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
- Optional `context`: current required project instruction input from the selected root
- Optional `operationGuidance`: current advisory guidance for apply
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change (if it is not installed, run `openspec status --change "<name>" --json` to see the next artifact and `openspec instructions <artifact-id> --change "<name>" --json` for how to create it)
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
Treat `context` as a required prompt-level input. Read and consider it, and
apply relevant project facts, conventions, and constraints while implementing.
Treat `operationGuidance` as optional additive advice. Read and consider every
entry, and follow entries that are applicable and compatible with the built-in
workflow.
Keep both fields separate from CLI-returned state, missing artifacts, tasks,
progress, `contextFiles`, and the built-in `instruction`. They are not
evidence of task completion, do not replace the built-in instruction, and do
not permit bypassing a blocked state. If context conflicts with the built-in
instruction, an explicit user choice, or a CLI-controlled value, report the
conflict and preserve the controlling value. If guidance is inapplicable or
conflicts with those controlling inputs, do not follow it and explain why.
These are prompt-level behavior contracts, not enforceable checks.
4. **Read context files**
Read every file path listed under `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
Do not copy `context` or `operationGuidance` verbatim into implementation
files or planning artifacts unless the user separately asks for that content.
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! Ready to archive this change.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
- Do not use context or operation guidance as proof that a task is complete
- Apply relevant project context; report conflicts with controlling workflow inputs
- Consider every guidance entry; explain any inapplicable or conflicting advice
- Do not copy runtime context or operation guidance into implementation files or planning artifacts
- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
@@ -0,0 +1,180 @@
---
name: openspec-archive-change
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Archive a completed change in the experimental workflow.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show only active changes (not already archived).
Include the schema used for each change if available.
Always announce: "Using change: <name>" and how to override (e.g., `$openspec-archive-change <other>`).
**Load current archive inputs before the existing archive checks:**
After resolving the selected change and planning root, run:
```bash
openspec instructions archive --change "<name>" --json
```
Keep the same selected-root flags on this command. This lookup is advisory and
optional: it only supplies extra prompt inputs, so it must never block archiving.
If it exits non-zero or returns invalid JSON — for example on an older CLI that
does not support this command yet — continue the archive workflow with no
context and no operation guidance. Do not report an error and do not stop.
A successful response may omit both optional fields. Treat `context` as a
required prompt-level input: read and consider it, and apply relevant project
facts, conventions, and constraints. Treat `operationGuidance` as optional
additive advice: read and consider every entry, and follow entries that are
applicable and compatible with the built-in archive workflow.
Keep both fields separate from built-in steps, explicit user choices, resolved
paths, CLI checks, and command contracts. If context conflicts with one of those
controlling inputs, report the conflict and preserve the controlling value. If
guidance is inapplicable or conflicts with a controlling input, do not follow it
and explain why. Do not infer replacement paths, skipped prompts, or flags from
either field, and do not copy their text verbatim into specs, change artifacts,
or archive summaries unless the user separately asks for it. These are
prompt-level behavior contracts, not enforceable checks.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
- `artifacts`: List of artifacts with their status (`done`, `skipped`, or other)
**If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs):
- Display warning listing incomplete artifacts
- Ask the user to confirm they want to proceed
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Ask the user to confirm they want to proceed
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only
delta-spec source. If the `specs` entry is missing or
`existingOutputPaths` is empty, proceed without a sync prompt and do not infer
delta specs from other artifacts.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `<planningHome.root>/openspec/specs/<capability>/spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path)
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
Route on the answer:
- "Cancel" — stop, do not archive
- "Archive without syncing" or "Archive now" — proceed to archive
- "Sync now" or "Sync anyway" — sync, then verify (below)
- Anything else — ask again rather than archiving
Before a selected sync writes any main spec, run
`openspec instructions specs --change "<name>" --json` once with the same
selected-root flags. Require a zero exit status and valid artifact-instruction
JSON. If the lookup fails or returns invalid JSON, report the error and stop
before writing any main spec or moving the change. A valid response with omitted
`rules` is the no-rules case. Apply returned `rules` only to the content and
form of main specs produced by this merge; do not use them as archive guidance,
change CLI behavior, or copy the rule text into any output file.
Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
Then re-run the comparison from the top of this step against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
- ADDED requirements present
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone
- RENAMED requirements present under the new name and absent under the old one
If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
5. **Perform the archive**
Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
```bash
mkdir -p "<planningHome.changesDir>/archive"
```
Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-<change-name>`. Never stack a second date (same rule as `openspec archive`).
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move `changeRoot` to the archive directory
```bash
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Whether specs were synced (if applicable)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```markdown
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped">
<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
```
**Guardrails**
- Announce the selected change; prompt for selection when it is ambiguous
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
- Consider every guidance entry and explain any inapplicable or conflicting advice
- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged
- Artifact rules constrain only the specs being written and are never operation guidance
- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files
+296
View File
@@ -0,0 +1,296 @@
---
name: openspec-explore
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
Then read the project's own context from the resolved root - `<root.path>/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists:
- `context`: project background - tech stack, conventions, constraints
- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact
Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Resolve and read existing artifacts for context**
- Run `openspec status --change "<name>" --json`.
- Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
- Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|----------------------------|--------------------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
You: Real-time collab is a big space. Let me think about this...
COLLABORATION SPECTRUM
════════════════════════════════════════════
Awareness Coordination Sync
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Presence│ │Cursors │ │ CRDT │
│ "3 │ │ Multi │ │Conflict│
│online" │ │ select │ │ free │
└────────┘ └────────┘ └────────┘
│ │ │
trivial moderate complex
Where's your head at?
```
**User brings a specific problem:**
```
User: The auth system is a mess
You: [reads codebase]
┌─────────────────────────────────────────────┐
│ CURRENT AUTH FLOW │
└─────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ Email │
│ OAuth │ │ OAuth │ │ Magic │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
▼
┌───────────┐
│ Session │
└─────┬─────┘
│
▼
┌───────────┐
│ Perms │
└───────────┘
I see three tangles. Which one's burning?
```
**User is stuck mid-implementation:**
```
User: $openspec-explore add-auth-system
The OAuth integration is more complex than expected
You: [reads change artifacts]
You're on task 4: "Implement OAuth flow"
Let me trace what's involved...
[draws diagram, explores options, suggests paths]
Want to update the design to reflect this?
Or add a spike task to investigate?
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
You: Generic answer is boring. What's the context?
User: A CLI tool that tracks local dev environments
You: That changes everything.
┌─────────────────────────────────────────────────┐
│ CLI TOOL DATA STORAGE │
└─────────────────────────────────────────────────┘
Key constraints:
• No daemon running
• Must work offline
• Single user
SQLite Postgres
Deployment embedded ✓ needs server ✗
Offline yes ✓ no ✗
Single file yes ✓ no ✗
SQLite. Not even close.
Unless... is there a sync component?
```
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When it feels like things are crystallizing, you might summarize:
```
## What We Figured Out
**The problem**: [crystallized understanding]
**The approach**: [if one emerged]
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change proposal
- Keep exploring: just keep talking
```
But this summary is optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own
+123
View File
@@ -0,0 +1,123 @@
---
name: openspec-propose
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is:
- proposal.md (what & why)
- `specs/<capability>/spec.md` (what the system must do - a delta, not the main spec)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run $openspec-apply-change
---
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Ask the user (open-ended, no preset options):
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on)
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
4. **Create every artifact in the required set**
Use a todo list to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
- An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
- Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
- Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped
c. **If an artifact requires user input** (unclear context):
- Ask the user to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
- What's ready: "All artifacts needed for implementation are ready."
- Prompt: "Run `$openspec-apply-change` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names
- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next
+223
View File
@@ -0,0 +1,223 @@
---
name: openspec-sync-specs
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show changes that have delta specs (under `specs/` directory).
Always announce: "Using change: <name>" and how to override (e.g., `$openspec-sync-specs <other>`).
2. **Resolve change context**
Run:
```bash
openspec status --change "<name>" --json
```
The JSON includes `planningHome.root`. Main specs live under `<planningHome.root>/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository.
3. **Find delta specs**
Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the
only source of delta spec paths. If the `specs` entry is missing or
`existingOutputPaths` is empty, report that there are no delta specs to sync,
do not infer them from other artifacts, and stop without requesting artifact
instructions or writing a main spec.
Sync every path in `existingOutputPaths` unless the caller narrowed the set.
A caller narrows it by naming an explicit list of delta spec paths to sync —
archive does this inline, and a user can too ("only sync the billing delta").
Then sync only the named paths and leave the remaining delta specs untouched:
bulk archive excludes a delta whose implementation it could not find, and
syncing it anyway would write a main spec the caller deliberately withheld.
Carry that narrowed selection through step 4; never widen it back to the full
list. If a named path is not in `existingOutputPaths`, do not sync it —
report it and stop, rather than dropping it silently. If the named list is
empty, report that there is nothing to sync and stop without writing a main
spec.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
4. **For each delta spec, apply changes to main specs**
Before the first main-spec write, obtain one current specs-rule snapshot:
- If archive invoked this workflow inline and supplied a valid snapshot from
`openspec instructions specs --change "<name>" --json`, reuse it and do not
fetch the same instructions again.
- Otherwise run that command once now with the same selected-root flags.
- If the direct lookup exits non-zero or returns invalid artifact-instruction
JSON, report the error and stop before writing any main spec. Do not treat the
failure as an absent rule set.
- A valid response with omitted `rules` means no artifact rules are configured
and the existing semantic merge continues.
Apply returned `rules` only to the content and form of the main specs produced
by this merge. Artifact rules are not operation guidance and cannot change
selected roots, delta paths, CLI checks, or workflow steps. Use their text as
constraints without copying it verbatim into a main spec or summary.
For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo):
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `<planningHome.root>/openspec/specs/<capability>/spec.md` (may not exist yet)
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**`## Purpose` in the delta:**
- The main spec already has one and it is authoritative - leave it alone
(this is what `openspec archive` does; it warns and moves on)
d. **Create new main spec** if capability doesn't exist yet:
- Create `<planningHome.root>/openspec/specs/<capability>/spec.md`
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
5. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
- Any new main spec left with a TBD Purpose placeholder, so it gets written
now rather than lingering
**Delta Spec Format Reference**
```markdown
## Purpose
Only on a delta that introduces a brand-new capability. Seeds the new main spec.
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Main Spec Format Reference**
Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section:
```markdown
# <capability> Specification
## Purpose
Short description of what this capability does and why it exists.
## Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
```markdown
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result
- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts
- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list
- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline
- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response
- Artifact rules constrain only the specs being written and are never copied into output files
@@ -0,0 +1,90 @@
---
name: openspec-update-change
description: Update an OpenSpec change by revising its existing planning artifacts and keeping them coherent with one another. Use when the user wants to revise a change's plan, fold new decisions into it, or reconcile its artifacts after an edit. Never edits code.
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.7.0"
---
Revise a change's existing planning artifacts and keep them coherent. Never edit code.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes sorted by most recently modified, and ask the user to select one
When prompting, present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from `schema` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from `lastModified` field)
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to update.
Always announce: "Using change: <name>" and how to override (e.g., `$openspec-update-change <other>`).
2. **Get the change's artifacts**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
The files to edit are `artifactPaths.<id>.existingOutputPaths` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. `specs/**/*.md`). Do NOT write to `resolvedOutputPath`: for a glob artifact it is still the glob pattern, not a real file.
3. **Understand the request**
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
- If they only said "update" / "make this coherent", treat it as a coherence review: read the existing artifacts and check them against each other for contradictions, gaps, and duplication.
4. **Read and reconcile**
- Read the artifact(s) the request touches and the change's other existing artifacts.
- Apply the requested edit. Then check every other existing artifact against it - in ANY direction: an edit to a later artifact may require revising an earlier one, not only the other way around. Build order is a useful reading order, not a constraint on which artifacts may be revised.
- Note everything that is now inconsistent, missing, or contradictory.
- Revise only files that already exist (`existingOutputPaths`). Do NOT create artifacts that don't exist yet, and do NOT invent new files under a glob artifact - note them and point the user to `$openspec-continue-change` to create them.
- If the change is already coherent, say so and make no edits.
5. **Confirm and apply, one artifact at a time**
- Show each proposed revision and why. Write only after the user confirms.
- If the user rejects a revision, do not write it - leave that artifact unchanged.
- When a substantial rewrite is needed, get that artifact's rules and template first:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
6. **Point to the next step (guidance only - NEVER act on it)**
- Artifacts still missing -> suggest `$openspec-continue-change` to create them.
- Change already implemented (tasks checked off / already applied) -> the code may no longer match the revised plan; suggest `$openspec-apply-change` to carry the delta into code.
- Everything done and implemented -> suggest `$openspec-archive-change`.
**Output**
After each invocation, show:
- Which artifacts were revised (and which proposed revisions were rejected)
- Anything deferred to `$openspec-continue-change` (not-yet-created artifacts or files)
- Where the change stands and the recommended next command
**Guardrails**
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, stop and point to `$openspec-apply-change`.
- Use the artifact ids and paths reported by `openspec status`; never branch on hardcoded artifact names.
- Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`.
- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is `$openspec-continue-change`'s job.
- Confirm every edit with the user before writing.
- If the request changes the change's *intent* rather than refining it, recommend starting fresh with `$openspec-new-change` (the "Update vs. Start Fresh" heuristic).
- `$openspec-continue-change` and `$openspec-new-change` may not be installed (core profile). When suggesting one that is unavailable, point to the CLI instead: `openspec status --change "<name>" --json` shows the next artifact and `openspec instructions <artifact-id> --change "<name>" --json` explains how to create it.
+14 -2
View File
@@ -6,5 +6,17 @@ build
logs/*.log
logs/*.log.gz
conf/*.conf
conf/*.xml
# Ignore all files under conf
conf/**
# Except all subdirectories so git will search all those
!conf/**/
# But do NOT ignore those 'example' files
!conf/**/*.example
# Ignore all files under data
data/**
# Except all subdirectories so git will search all those
!data/**/
# But do NOT ignore those 'example' files
!data/**/*.example
+71
View File
@@ -0,0 +1,71 @@
# Agent instructions
## Gitea issues
When a task refers to `issue#<number>` or `issue #<number>`, read the issue with:
```bash
curl -fsS \
"https://git.r35157.com/api/v1/repos/r35157/com_r35157_nenjim-hubd-impl_ref/issues/<number>"
```
Treat the issue title and body as the authoritative task description.
Gitea access is read-only unless the user explicitly requests otherwise.
## ValueTags
Text enclosed by `Ω`, such as `ΩRaydiumLiquidityΩ`, is a ValueTag.
ValueTags provide semantic type safety without requiring a separate Java
record or wrapper class for every domain value.
Values with the same Java backing type may represent different domain
concepts and must not necessarily be interchangeable. ValueTags help prevent
accidentally swapped arguments and express knowledge about what a value means,
for example whether a price includes tax.
Choose ValueTags according to their domain meaning and the contract of the
method, not merely according to their generated Java backing type.
The ValueTag hierarchy in `conf/detag.conf` determines its Java backing type.
For example, `ΩRaydiumLiquidityΩ` is generated as `BigInteger`.
Imports required by the generated Java type must remain in the `.tjava`
source, even when the backing type name does not appear directly in that file.
Edit files under `src/main/tjava`.
Do not edit generated files under `build/generated/sources/detag`.
## Detag configuration
Detagging is the process of preprocessing `.tjava` files into `.java` files
by resolving ValueTags to their Java backing types.
The Detag configuration is available at:
`conf/detag.conf`
This file is a symbolic link to the shared configuration outside the repository.
Detag processing is integrated into the normal Gradle build and runs
automatically when the project is built.
Read `conf/detag.conf` before working with `.tjava` files or ValueTags.
Do not run Detag manually unless explicitly required.
Do not modify `conf/detag.conf` unless the user explicitly requests it.
When writing new `.tjava` code, prefer an existing ValueTag from
`conf/detag.conf` when it accurately describes the value.
Use the most specific appropriate ValueTag. Do not use a semantically
incorrect ValueTag merely because it has the desired Java backing type.
When no suitable ValueTag exists, propose a new ValueTag with:
- its name
- its parent in the ValueTag hierarchy
- its Java backing type
- the reason it is needed
Do not add or change ValueTags in `conf/detag.conf` without explicit user
approval.
+6 -1
View File
@@ -17,7 +17,7 @@ Every application-owned, human-maintained runtime configuration file starts
with an explicit format version:
```text
FORMAT_VERSION=1
FORMAT_VERSION=<version supported by that file's loader>
```
Blank lines and full-line comments may precede the declaration, but it must be
@@ -33,6 +33,7 @@ configuration files. Compatible additions do not require a version increment.
This policy currently applies to:
- `NenjimHub.conf` (mandatory version 1 with ordered `[AUTO_START]`)
- `alarms.conf`
- `alarmaction_Console.conf`
- `alarmaction_Pushover.conf`
@@ -44,6 +45,10 @@ This policy currently applies to:
Third-party configuration formats such as Log4j's XML configuration are
versioned by their owning library and do not use this declaration.
Nenjim Hub reads only `conf/NenjimHub.conf`; copy
`conf/NenjimHub.conf.example` before first use and edit the ordered process
list for the deployment role. The example is never loaded as runtime input.
## Release log:
| Date | Version | Description |
|:-----------------|:--------------:|----------------------|
+144 -47
View File
@@ -4,28 +4,41 @@ The Jupiter Perps Price Alarm monitors the Jupiter Perps aggregated oracle accou
Oracle prices are received through Solana WebSocket `accountSubscribe` using `processed` commitment. The application does not poll prices once per second. It also fetches the current oracle account state whenever a WebSocket connection opens, reconnects automatically, and supports multiple RPC endpoints for redundancy.
NenjimHub knows how to construct the alarm as
`jupiter-perps-alarm.default`, but does so only when that ID is explicitly
listed in `conf/NenjimHub.conf` under `[AUTO_START]`. Its passive
`SolanaWallet` and `JupiterPerpsService` dependencies are selected by the Hub
and injected before registration. The alarm action configuration files do not
select a wallet or signing key.
> **Important:** This application is not necessarily notification-only. Every active entry in a Jupiter Perps position action file can sign and submit a real transaction. Review both transaction action files before running it.
## Startup flow
The application performs the following steps before it starts monitoring prices:
The alarm performs the following steps before it starts monitoring prices:
1. Parse command-line options and the alarm configuration file.
2. Fetch the wallet's current open Jupiter Perps positions.
3. Populate entry-price and liquidation-price variables.
1. Parse the configured alarm path, WebSocket endpoint environment and
`alarms.conf`.
2. Parse the position-increase and position-decrease action files, including
their structure and reserve settings.
3. Fetch the injected wallet's current open Jupiter Perps positions and populate
entry-price and liquidation-price variables.
4. Resolve and validate every alarm condition.
5. Start the periodic variable refresher and file-trigger watcher.
6. Parse and validate the four action configuration files.
6. Parse the console and Pushover action files and construct all alarm actions.
7. Open one oracle WebSocket connection per configured asset and RPC endpoint.
Startup fails before WebSockets are opened if an alarm condition or action configuration contains an unresolved variable or invalid value.
Startup fails before WebSockets are opened if an alarm condition or an action
value that is resolved during startup contains an unavailable variable or
invalid value. Jupiter transaction-row expressions are resolved and validated
when their alarm triggers.
## Requirements and build
The project uses:
- JDK 25
- Gradle 9.3.1 through the Gradle wrapper
- Gradle 9.6.1 through the Gradle wrapper
- Java preview features
- Detag-generated Java sources from `src/main/tjava`
@@ -43,35 +56,53 @@ To create the application JAR and collect runtime dependencies in `build/libs`:
There is currently no alarm-specific automated self-test task.
## Running the alarm application
## Running through Nenjim
The alarm entry point is:
The alarm no longer has a standalone `main` method. Its default Hub template
intentionally omits the alarm, so it is neither constructed, registered nor
started unless the runtime `[AUTO_START]` section contains:
```text
com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl
jupiter-perps-alarm.default
```
The Gradle application plugin currently points at the Nenjim Hub main class, so `./gradlew run` does **not** directly start the alarm application. Run `JupiterPerpsAlarmImpl` from the IDE, or run the prepared JAR and dependencies explicitly:
Registry and Journal must remain the first two configured processes. The
alarm's current wallet and Jupiter service dependencies are passive, so they
are available after Registry bootstrap. Run the configured Hub with:
```bash
./gradlew run
```
After `./gradlew prepareLibs`, the same Hub can be invoked with its main class:
```bash
java --enable-preview \
-cp "build/libs/*" \
com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl \
--config=conf/alarms.conf
com.r35157.nenjim.hubd.Main
```
The repository contains example files for the alarm configuration and every action configuration. The real `conf/*.conf` files are ignored by Git; keep credentials and operational wallet details out of version control.
The current Hub constructor binds the selected alarm to `conf/alarms.conf` and
to the test Jupiter Perps wallet and service. `[AUTO_START]` controls only
whether and when the known process is constructed; a different alarm path,
wallet, or service still requires composition-code changes rather than an alarm
action-file setting.
### Command-line options and environment variables
The repository contains example files for the alarm configuration and every
action configuration. The real `conf/*.conf` files are ignored by Git; keep
credentials and operational details out of version control.
| Purpose | Command-line option | Environment variable | Default |
|---|---|---|---|
| Alarm configuration | `--config=<path>` | `PRICE_ALARMS_CONFIG` | `price-alarms.conf` |
| Solana WebSocket endpoints | `--ws=<url1,url2,...>` | `SOLANA_WS_URLS` | `wss://api.mainnet-beta.solana.com` |
### Environment variables
The command-line option takes precedence over its environment variable.
The Hub does not currently forward alarm-specific command-line options. Solana
WebSocket endpoints can be overridden with:
The variable-refresh file watcher uses the configuration file's parent directory. Until that is changed, use a configuration path with an explicit directory component, such as `conf/alarms.conf` or `./price-alarms.conf`, rather than relying on the bare default filename.
```bash
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
```
If `SOLANA_WS_URLS` is absent, the alarm uses
`wss://api.mainnet-beta.solana.com`.
## Configuration files
@@ -79,11 +110,11 @@ The application uses five configuration files:
| File | Purpose |
|---|---|
| `alarms.conf` | Variables and price-alarm definitions |
| `alarms.conf` | Shared variables and price-alarm definitions |
| `alarmaction_Console.conf` | Alarm IDs written to the console |
| `alarmaction_Pushover.conf` | Pushover credentials and notifications |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | Jupiter wallet, signer, and position increases |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | Jupiter wallet, signer, and position decreases |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | USDC reserve and position increases |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | Position-size reserve and position decreases |
The four action files are resolved in the same directory as the file supplied through `--config`. All five files are required at startup, although an action can contain no alarm IDs. Blank lines and lines beginning with `#` are ignored.
@@ -97,12 +128,16 @@ Example files:
### Format version
Every configuration file must declare its format version as its first actual
configuration entry:
Every configuration file must declare its own format version as its first
actual configuration entry:
```text
FORMAT_VERSION=1
```
| File | Format version |
|---|---:|
| `alarms.conf` | `2` |
| `alarmaction_Console.conf` | `1` |
| `alarmaction_Pushover.conf` | `1` |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | `2` |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | `2` |
Blank lines and full-line comments may appear before it. The application
rejects a missing, duplicate, malformed, misplaced, older, or newer version
@@ -121,12 +156,15 @@ the project-wide policy.
Variable definitions live in `alarms.conf`. A definition consists of a name and a single-token value:
```text
{{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
{{SOL_WARNING_PRICE}} 150.00
```
Variable names must use uppercase letters, digits, and underscores. User-defined variables can be referenced from alarm conditions and action configuration values as `{{NAME}}`.
`JUPITER_PERPS_WALLET` is required for the initial and periodic Jupiter position fetches. For open SOL, BTC, and ETH positions, the refresher supplies these dynamic variables:
The wallet address used for initial and periodic Jupiter position fetches comes
from the injected `SolanaWallet`; it is not a shared configuration variable.
For open SOL, BTC, and ETH positions belonging to that wallet, the refresher
supplies these dynamic variables:
```text
{{SOL_LONG_ENTRY_PRICE}} {{SOL_LONG_LIQ_PRICE}}
@@ -137,9 +175,17 @@ Variable names must use uppercase letters, digits, and underscores. User-defined
{{ETH_SHORT_ENTRY_PRICE}} {{ETH_SHORT_LIQ_PRICE}}
```
The refresher replaces user-supplied variables with these names when position data is fetched. Only variables for positions returned by Jupiter are populated. Startup validation fails if a condition or action value references a variable that is unavailable after the initial position fetch.
The refresher replaces user-supplied variables with these names when position
data is fetched. Only variables for positions returned by Jupiter are
populated. Startup validation fails if an alarm condition references a variable
that is unavailable after the initial position fetch. Transaction-row
expressions are resolved later, when their alarm triggers.
Alarm conditions, Pushover notes, and Jupiter position-increase rows are resolved again when an alarm triggers, so refreshed values take effect without restarting the application. Pushover credentials and severity plus the Jupiter wallet and signer are resolved during startup.
Alarm conditions, Pushover notes, and Jupiter position-increase and
position-decrease rows are resolved again when an alarm triggers, so refreshed
values take effect without restarting the application. Pushover credentials
and severity are resolved during startup. The wallet and signer belong to the
injected `SolanaWallet` and are not resolved from alarm configuration.
## Alarm definitions
@@ -362,13 +408,13 @@ Credentials, severity, and notes may reference shared variables. Credentials and
### Automatic position increase
`alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains the wallet and signer followed by zero or more position increases:
`alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains a minimum
USDC reserve followed by zero or more position increases:
```text
FORMAT_VERSION=1
FORMAT_VERSION=2
WALLET_ID {{JUPITER_PERPS_WALLET}}
SIGNER_KEY_NAME <jup-key-name>
RESERVE_USDC_LIMIT 10
# ID ASSET DIRECTION COLLATERAL_USDC SIZE_DELTA_USD MAX_SLIPPAGE_BPS
4 SOL LONG 0.25 2.50 200
@@ -382,11 +428,55 @@ Each active row can submit a real transaction when its alarm triggers. Supported
- `SIZE_DELTA_USD`: zero or greater.
- `MAX_SLIPPAGE_BPS`: from `0` to `10000`.
Wallet, signer, asset, direction, amounts, and slippage may reference shared variables. Wallet and signer are resolved during startup. Every transaction row is validated during startup and resolved again at trigger time, allowing refreshed variables to change the transaction parameters without a restart.
`RESERVE_USDC_LIMIT` is the amount of USDC that must remain after collateral is
deducted. An increase is skipped unless the injected wallet's USDC balance can
cover both the requested collateral and this reserve.
For a configured ID, the action builds a Jupiter position-increase transaction, signs it by running `jup sign -f json --key <SIGNER_KEY_NAME> --tx <serialized-transaction>`, verifies that the returned signer matches `WALLET_ID`, and submits the signed transaction. IDs absent from this file never perform a transaction.
Asset, direction, amounts, and slippage may reference shared variables. Every
transaction row is structurally parsed during startup, then resolved and
validated at trigger time. This allows refreshed variables to change the
transaction parameters without a restart.
The example transaction rows are commented out. Do not activate a row until its alarm condition, wallet, signer, asset, direction, amounts, and slippage have been reviewed.
For a configured ID, the injected `JupiterPerpsService` builds the position
increase, uses its injected wallet to sign it, submits it and awaits the
transaction confirmation. IDs absent from this file never perform a
transaction.
The example transaction rows are commented out. Do not activate a row until
its alarm condition, injected wallet binding, asset, direction, amounts,
reserve, and slippage have been reviewed.
### Automatic position decrease
`alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` contains a minimum
remaining position-size reserve followed by zero or more position decreases:
```text
FORMAT_VERSION=2
RESERVE_POSITION_SIZE_USD_LIMIT 10
# ID ASSET DIRECTION RECEIVE_TOKEN SIZE_DELTA_USD MAX_SLIPPAGE_BPS
3 SOL SHORT USDC 5.00 200
```
Supported values are:
- `ASSET`: `SOL`, `ETH`, or `BTC`.
- `DIRECTION`: `LONG` or `SHORT`.
- `RECEIVE_TOKEN`: `USDC` or the token matching `ASSET`.
- `SIZE_DELTA_USD`: greater than zero.
- `MAX_SLIPPAGE_BPS`: from `0` to `10000`.
The action finds the injected wallet's single matching open position. It skips
the decrease if the resulting position size would be below
`RESERVE_POSITION_SIZE_USD_LIMIT`. Asset, direction, receive token, amount and
slippage may reference shared variables and are resolved again at trigger time.
For a configured ID, the injected `JupiterPerpsService` builds the position
decrease, uses its injected wallet to sign it, submits it and awaits the
transaction confirmation. IDs absent from this file never perform a
transaction.
## RPC redundancy and reconnect behavior
@@ -407,7 +497,11 @@ Each connection:
## Validation and errors
Structural configuration errors include the file path and line number. After the initial Jupiter position fetch, every condition is resolved and parsed. The action files are then parsed and validated before WebSockets are opened.
Structural configuration errors include the file path and line number. The two
Jupiter action files are parsed before the initial position fetch. After that
fetch, every alarm condition is resolved and parsed, and the console and
Pushover files are loaded before WebSockets are opened. Jupiter transaction-row
expressions are resolved and validated only when their alarm triggers.
Startup validation rejects, among other things:
@@ -418,12 +512,14 @@ Startup validation rejects, among other things:
- missing range targets;
- negative target values or percentages;
- ranges whose lower target exceeds the upper target;
- missing action credentials or wallet settings;
- missing action credentials or reserve settings;
- duplicate action IDs;
- invalid Pushover severities;
- invalid Jupiter assets, directions, amounts, or slippage.
- invalid Pushover severities.
At runtime, a later condition resolution/parsing failure skips that price event for the affected alarm. A later action-variable failure is logged by the action dispatcher, and the remaining actions are still attempted.
At runtime, a later condition resolution/parsing failure skips that price event
for the affected alarm. Invalid Jupiter assets, directions, receive tokens,
amounts, slippage, or unavailable action variables are logged by the action
dispatcher, and the remaining actions are still attempted.
## Current limitations
@@ -434,6 +530,7 @@ At runtime, a later condition resolution/parsing failure skips that price event
- Target expressions support only a decimal value with an optional single percentage adjustment; they are not a general expression language.
- Configuration files are read only during startup; editing an action file requires a restart.
- Action IDs are not cross-validated against `alarms.conf`; an unknown ID is accepted but never triggered.
- Pushover credentials and severity plus the Jupiter wallet and signer are resolved only during startup. Dynamic re-resolution applies to conditions, Pushover notes, and Jupiter transaction rows.
- A bare default configuration path has no parent directory for the refresh watcher; use an explicit path such as `./price-alarms.conf` or `conf/alarms.conf`.
- Pushover credentials and severity are resolved only during startup. Dynamic
re-resolution applies to conditions, Pushover notes, and Jupiter transaction
rows. Wallet and signer selection requires changing the Hub composition.
- This application is a monitoring and automation aid, not a substitute for independent risk controls.
+1
View File
@@ -0,0 +1 @@
perl -0777 -pi -e 's/,\s+/,/g; s/\s+\z//' $1
+10 -2
View File
@@ -11,7 +11,7 @@ if (version == "UNSET" && gradle.startParameter.taskNames.any { it.startsWith("p
}
application {
mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main")
mainClass.set("com.r35157.nenjim.hubd.Main")
applicationDefaultJvmArgs = listOf("--enable-preview")
}
@@ -41,6 +41,7 @@ val detag = configurations.create("detag") {
dependencies {
detag("com.r35157.tools:detag-impl_ref:0.1.0")
compileOnly("org.jetbrains:annotations:26.1.0")
testCompileOnly("org.jetbrains:annotations:26.1.0")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
@@ -51,6 +52,9 @@ dependencies {
implementation("org.apache.commons:commons-collections4:4.5.0")
implementation("org.apache.commons:commons-lang3:3.20.0")
implementation("org.slf4j:slf4j-api:2.0.18")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.13.4")
}
java {
@@ -69,6 +73,10 @@ tasks.withType<JavaCompile>().configureEach {
)
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
val cleanGeneratedDetagMain = tasks.register<Delete>("cleanGeneratedDetagMain") {
@@ -82,7 +90,7 @@ val detagMain = tasks.register<JavaExec>("detagMain") {
classpath = detag
mainClass.set("com.r35157.tools.detag.impl.ref.Main")
val configFile = layout.projectDirectory.file("../detag.conf")
val configFile = layout.projectDirectory.file("conf/detag.conf")
val sourceRoot = layout.projectDirectory.dir("src/main/tjava")
inputs.file(configFile)
+8
View File
@@ -0,0 +1,8 @@
# Copy this template to conf/NenjimHub.conf before starting Nenjim Hub.
# Registry and Journal are mandatory and must be the first two entries.
# Entries are ordered. List every process dependency manually before its consumers.
FORMAT_VERSION=1
[AUTO_START]
nenjim.registry.service
nenjim.journal.service
@@ -1,9 +1,5 @@
FORMAT_VERSION=1
FORMAT_VERSION=2
# Wallet and signer used for position-decrease transactions
################################################################
WALLET_ID {{JUPITER_PERPS_WALLET}}
SIGNER_KEY_NAME <SIGNER>
RESERVE_POSITION_SIZE_USD_LIMIT 10
# Id Asset Direction ReceiveToken SizeDeltaUSD MaxSlippageBPS
@@ -1,9 +1,5 @@
FORMAT_VERSION=1
FORMAT_VERSION=2
# Wallet and signer used for position-increase transactions
################################################################
WALLET_ID {{JUPITER_PERPS_WALLET}}
SIGNER_KEY_NAME <SIGNER>
RESERVE_USDC_LIMIT 10
# Id Asset Direction CollateralUSDC SizeDeltaUSD MaxSlippageBPS
+1 -6
View File
@@ -1,9 +1,4 @@
FORMAT_VERSION=1
# General configurations
#####################################################
{{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
FORMAT_VERSION=2
# Constant Name Value
#####################################
+85
View File
@@ -0,0 +1,85 @@
# Ω
AssetPrice
SyrupPrice
BigDecimal
Amount
USDCAmount
CurveWidth
Price
USDCPrice
RaydiumSqrtPrice
BigInteger
JupiterCumulativeInterestRate
JupiterCumulativeInterestSnapshot
JupiterPostionInterest
Liquidity
RaydiumLiquidity
RaydiumSqrtPriceX64
byte[]
JupiterIdl
JupiterPerpsCustodyAccountIdl
JupiterPerpsPostionIdl
int
amountDecimals
portNumber
raydiumLiquidityTickIndex
seconds
long
jupiterRawEntryPriceUsd
jupiterRawCollateralUsd
jupiterRawSizeUsd
lamports
milliSeconds
Long
ContextId
SolanaSlot
MoneyAmount
SolanaAmount
SyrupAmount
SolanaAccountInfo
JupiterCustodyAccountInfoEncoded
JupiterPerpsPositionInfoEncoded
String
APIKey
Checksum
SHA256Checksum
CryptoCurrencyAddress
SolanaAddress
JupiterPerpsCustodyAccountAddress
JupiterPerpsCollateralCustodyAccountAddress
JupiterPerpsPositionAccount
LiquidityPoolId
RaydiumLiquidityPoolId
RaydiumLiquidityPoolConcentratedId
RaydiumLiquidityPoolStandardId
RaydiumLiquidityPoolAccount
RaydiumLiquidityPoolPositionId
SPLMintAddress
RaydiumLiquidityPoolPositionMintId
SolanaProgramId
JupiterPerpsProgramId
RaydiumProgramId
SPLProgramId
SPLTokenAccount
SolanaNFTAddress
RaydiumLiquidityPoolPositionNftId
SolanaWalletId
EmailAddress
EmailSubject
EncodedString
Base58String
SolanaTransactionSignature
Base64String
SolanaBlockhash
Hostname
Name
PriceSourceName
NotificationMessage
Password
PhoneNumber
RawAmount
UserAgent
UserName
URI
RestEndpoint
RestEndpointBase
@@ -0,0 +1,23 @@
# Nenjim Hub public API Journal, format version 1.
FORMAT_VERSION=1
JOURNAL_VERSION=20260816201156858Z
[METADATA]
GROUP=com_r35157_nenjim
MODULE=hubd
ARTIFACT=api
DESCRIPTION='The public API for Nenjim Hub.'
INFORMATION_URI='ftp://ftp.r35157.com/nenjim/hubd/readme.txt'
[POLICY]
RECOMMENDED=[0.1.0]:'The best-tested API release'
DISCOURAGED=[0.0-->0.0]:'Pre-release design experiments'
BLACKLIST=0.0.3:'Known incompatible prototype'
[RELEASE 0.1.0]
LICENSE='Public Domain'
PUBLISHED_AT=20260815193000000Z
CONTENT_DIGEST=sha256:0000000000000000000000000000000000000000000000000000000000000001
CONTENT_DIGEST=cid1:bafkreibxz44g7iprf6gnh44giksyokavlxwicdpqcmqxmoobejhm5grm4a
CONTENT_SIZE=123456
@@ -0,0 +1,23 @@
FORMAT_VERSION=1
JOURNAL_VERSION=20260816201156858Z
[METADATA]
GROUP=com_r35157_nenjim
MODULE=hubd
ARTIFACT=impl_ref
DESCRIPTION='The reference implementation of Nenjim\'s Hub.'
INFORMATION_URI='ipfs:bafkreibxz44g7iprf6gnh44giksyokavlxwicdpqcmqxmoobejhm5grm4a'
[POLICY]
RECOMMENDED=0.1.0:'Reference release for the 0.1 line'
DISCOURAGED=[0.0.0-->0.0.9]:'Obsolete implementation experiments'
[RELEASE 0.1.0]
LICENSE='Public Domain'
PUBLISHED_AT=20260815194500000Z
CONTENT_DIGEST=sha256:0000000000000000000000000000000000000000000000000000000000000002
CONTENT_SIZE=234567
DEPENDS_ON=nenjim:com_r35157_nenjim-hubd-api=[0.1-->
EXCLUDES=nenjim:com_r35157_nenjim-hubd-api=[0.1.1-->0.1.2]:'Known binary incompatibility'
PREFERRED=nenjim:com_r35157_nenjim-hubd-api=0.1.0:'Built and reviewed together'
@@ -0,0 +1,21 @@
FORMAT_VERSION=1
JOURNAL_VERSION=20260816201156858Z
[METADATA]
GROUP=com_r35157_nenjim
MODULE=hubd
ARTIFACT=tests
DESCRIPTION='The reusable conformance tests for Nenjim Hub.'
[POLICY]
RECOMMENDED=0.1.0:'Current conformance suite'
[RELEASE 0.1.0]
LICENSE='Public Domain'
PUBLISHED_AT=20260815200000000Z
CONTENT_DIGEST=sha256:0000000000000000000000000000000000000000000000000000000000000003
CONTENT_SIZE=3456
DEPENDS_ON=nenjim:com_r35157_nenjim-hubd-api=[0.1]
PREFERRED=nenjim:com_r35157_nenjim-hubd-api=[0.1.0]:'Primary API target'
DEPENDS_ON=nenjim:com_r35157_nenjim-hubd-impl_ref=0.1.0
+1 -1
View File
@@ -21,4 +21,4 @@ exec java \
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
-Dlog4j.configurationFile=conf/log4j2.xml \
-cp "$CLASSPATH" \
com.r35157.nenjim.hubd.impl.ref.Main
com.r35157.nenjim.hubd.Main
+244
View File
@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AssetAZ Documentation</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
}
.container {
width: 80%;
max-width: 1000px;
margin: auto;
padding: 20px;
}
h1, h2 {
color: #0056b3;
}
.section {
margin-bottom: 40px;
}
.highlight {
background-color: #e7f1ff;
padding: 10px;
border-left: 4px solid #0056b3;
margin-top: 20px;
}
.footer {
text-align: center;
padding: 20px;
border-top: 1px solid #ddd;
margin-top: 40px;
color: #777;
}
.token-logo {
width: 100px;
height: 100px;
}
ul.toc {
background: #e7f1ff;
padding: 16px;
margin-bottom: 40px;
border-left: 4px solid #0056b3;
list-style: none;
}
ul.toc li {
margin-bottom: 6px;
}
ul.toc a {
color: #0056b3;
text-decoration: none;
}
ul.toc a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>AssetAZ Documentation</h1>
<ul class="toc">
<li><a href="#1-introduction-and-purpose">1. Introduction and Purpose</a></li>
<li><a href="#2-hubs-and-terminals">2. Hubs and Terminals</a></li>
<li><a href="#3-plugins-and-interfaces">3. Plugins and Interfaces</a></li>
<li><a href="#4-interaction-with-nenjim">4. Interaction with Nenjim</a></li>
<li><a href="#5-observables-and-subscribers">5. Observables and Subscribers</a></li>
<li><a href="#6-assetaz-token-and-incentives">6. AssetAZ Token and Incentives</a></li>
<li><a href="#7-peer-to-peer-and-oracles">7. Peer-to-peer and Oracles</a></li>
<li><a href="#8-user-experience">8. User Experience</a></li>
<li><a href="#9-transparency-trust-and-signed-builds">9. Transparency, Trust and Signed Builds</a></li>
<li><a href="#10-distribution-and-installation">10. Distribution and Installation</a></li>
<li><a href="#11-future-topics-todo">11. Future Topics (TODO)</a></li>
</ul>
<div class="section" id="1-introduction-and-purpose">
<h2>1. Introduction and Purpose</h2>
<p>
AssetAZ is <strong>not</strong> a finance application – it is a platform. Just like Windows is not Notepad
or Word, AssetAZ is not a budgeting app or a wallet. AssetAZ is the foundation upon which other applications
and plugins are built.
</p>
<p>
Each user (human or AI) runs their own “AssetHub” – the central system that manages all their financial
plugins and data. This means that all interaction with assets happens locally and under full user or AI
control. AssetAZ is designed as a new standard for building financial software – open, decentralized, and
flexible.
</p>
</div>
<div class="section" id="2-hubs-and-terminals">
<h2>2. Hubs and Terminals</h2>
<p>
An AssetHub runs in a single place – locally or in the cloud – and has one owner. Terminals are the devices
that access the hub: this could be mobile apps, API clients, smartwatches, or even automated agents such as
AIs. Some terminals have graphical UIs, others are completely invisible and operate only through messages or
events.
</p>
<p>
There is only <strong>one user per AssetAZHub</strong>. It is not a multi-user system, but a personal
system. Access to external systems (such as API keys) is managed in Nenjim, not inside AssetAZ.
</p>
</div>
<div class="section" id="3-plugins-and-interfaces">
<h2>3. Plugins and Interfaces</h2>
<p>
AssetAZ itself can do almost nothing. All functionality comes from plugins. A plugin can be, for example, a
wallet, a budgeting tool, or a plugin that connects to an exchange.
</p>
<p>
Plugins communicate via API modules (interfaces), which can have many different implementations. This
ensures that various components can be swapped out without breaking the whole system. A key feature of the
AssetAZ ecosystem is that <strong>interfaces survive, but implementations can die</strong>.
</p>
</div>
<div class="section" id="4-interaction-with-nenjim">
<h2>4. Interaction with Nenjim</h2>
<p>
Nenjim is responsible for fetching, configuring, and delivering instances of components. AssetAZ simply
asks Nenjim: “Give me a Bitcoin wallet.” Nenjim returns an instance that already works, because it’s
configured with, for example, the correct private key.
</p>
<p>
All configuration – such as access to external APIs – happens in Nenjim. Plugins in AssetAZ communicate
with Nenjim via lazy-loaded and versioned components. This enables side-by-side execution of old and new
versions of the same objects.
</p>
</div>
<div class="section" id="5-observables-and-subscribers">
<h2>5. Observables and Subscribers</h2>
<p>
Observables are plugins that provide information or events to others interested in receiving that
information. Those who listen to the information are called 'Subscribers'. When everything flows through
AssetAZ, it becomes possible to do centralized logging, analysis, and, for example, correct tax calculation
(FIFO, etc.).
</p>
<p>
A plugin acting as an Observable can also be a system that registers external events (such as water
temperature). Observables can also be subscribed to over P2P – an AssetAZHub can subscribe to an observable
from another hub and receive events as if they were local.
</p>
</div>
<div class="section" id="6-assetaz-token-and-incentives">
<h2>6. AssetAZ Token and Incentives</h2>
<p>
The AssetAZ token is used to pay developers, vote on decisions, and provide access to plugins and services.
When a task needs to be solved, the price is estimated in tokens. The developer is then paid in tokens and
can choose to keep them as an investment or sell them.
</p>
<p>
Tokens can also be used by users who wish to have features developed, or as a reward for contributing
knowledge, code, or data.
</p>
<p>
The token’s price is determined indirectly through the “anchors” that users set themselves – one user may
expect one token to equal one krone, while another may value it more highly. These differences create trade
and speculation on a decentralized exchange, but can also easily be traded “over the counter” (OTC).
</p>
</div>
<div class="section" id="7-peer-to-peer-and-oracles">
<h2>7. Peer-to-peer and Oracles</h2>
<p>
All AssetAZHubs run in a P2P network. This makes it possible to share services – for example, access to
wallets, sensor data, or plugins – without giving access to the actual code. You can sell oracle services,
such as measuring sea temperature every morning and making it available as an observable to other
AssetAZHubs.
</p>
<p>
This makes it possible to earn tokens on other types of assets than just financial – knowledge and data
also become assets.
</p>
</div>
<div class="section" id="8-user-experience">
<h2>8. User Experience</h2>
<p>
The user experience in AssetAZ depends 100% on the terminal and plugins you use. An AI will never use a UI,
while a human user may only use a smartwatch.
</p>
<p>
A plugin can choose to render information in many ways – it’s similar to installing a graphics card in a
computer. It translates data into something the user understands. It’s up to the terminal to present it.
</p>
</div>
<div class="section" id="9-transparency-trust-and-signed-builds">
<h2>9. Transparency, Trust and Signed Builds</h2>
<p>
All plugins are open source. The user can compile them themselves or trust builds that others have
published and cryptographically signed.
</p>
<p>
The user defines their own prioritized list of whom they trust. When a plugin needs to be installed, Nenjim
tries to find a build that matches from one of the trusted signatures.
</p>
<p>
It’s also possible to charge for signed builds, e.g., via the AssetAZ token, but this requires actively
adding a payment plugin – Nenjim has no dependency on AssetAZ unless you want it.
</p>
</div>
<div class="section" id="10-distribution-and-installation">
<h2>10. Distribution and Installation</h2>
<p>
To make AssetAZ easy to install, it can be delivered as a ready-made image with Lightwale – a minimalist,
immutable Linux system with Docker. There is only one container: Nenjim. It is configured to automatically
start AssetAZ and the relevant plugins via a single configuration file.
</p>
<p>
This makes it possible to deliver an entire hub with everything preconfigured – you just power it up.
</p>
</div>
<div class="section" id="11-future-topics-todo">
<h2>11. Future Topics (TODO)</h2>
<ul>
<li>Backup and restore via IPFS and encryption</li>
<li>Security and privacy section</li>
<li>AI as users and agents</li>
<li>Governance and community</li>
<li>User scenarios and fictional cases</li>
<li>Semver-enforcer tool and build validation</li>
<li>Further development of the observable concept</li>
<li>Fluid trust and signature replacement</li>
<li>Payment services for signed builds</li>
</ul>
</div>
<div class="footer">
<p>AssetAZ – The foundation for open, decentralized, and programmable finance.</p>
</div>
</div>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
#<center><H1>AssetAZ<br>Documentation</H1></center>
**Indholdsfortegnelse:**
<!-- TOC -->
* [1. Introduktion og formål](#1-introduktion-og-formål)
* [2. Hubs og terminaler](#2-hubs-og-terminaler)
* [3. Plugins og interfaces](#3-plugins-og-interfaces)
* [4. Interaktion med Nenjim](#4-interaktion-med-nenjim)
* [5. Observables og Subscribers](#5-observables-og-subscribers)
* [6. AssetAZ-token og incitamenter](#6-assetaz-token-og-incitamenter)
* [7. Peer-to-peer og orakler](#7-peer-to-peer-og-orakler)
* [8. Brugeroplevelse](#8-brugeroplevelse)
* [9. Transparens, tillid og signerede builds](#9-transparens-tillid-og-signerede-builds)
* [10. Distribution og installation](#10-distribution-og-installation)
* [11. Fremtidige emner (TODO)](#11-fremtidige-emner-todo)
<!-- TOC -->
## 1. Introduktion og formål
AssetAZ er ikke en økonomiapplikation – det er en platform. Ligesom Windows ikke er Notepad eller Word, er AssetAZ
ikke et budgetprogram eller en wallet. AssetAZ er selve fundamentet, som andre applikationer og plugins bygges ovenpå.
Hver bruger (menneske eller AI) kører sin egen “AssetHub” – det centrale system der håndterer alle ens økonomiske
plugins og data. Det betyder, at al interaktion med aktiver foregår lokalt og under fuld bruger- eller AI-kontrol.
AssetAZ er tænkt som en ny standard for at bygge økonomisk software – åben, decentral og fleksibel.
## 2. Hubs og terminaler
En AssetHub kører ét sted – lokalt eller i skyen – og har én ejer. Terminaler er de enheder, som tilgår hubben:
Det kan være mobilapps, API-klienter, smartwatches, eller endda automatiserede agenter som AI’er. Nogle terminaler
har grafisk UI, andre er helt usynlige og virker kun gennem beskeder eller hændelser.
Der er kun én bruger pr. AssetAZHub. Det er ikke et multibrugersystem, men et personligt system. Adgang til
eksterne systemer (som API-nøgler) styres i Nenjim og ikke inde i AssetAZ.
## 3. Plugins og interfaces
AssetAZ selv kan næsten ingenting. Alt funktionalitet kommer fra plugins. Et plugin kan være fx en wallet,
et budgetværktøj eller et plugin der forbinder til en børs.
Plugins kommunikerer gennem API-moduler (interfaces), som kan have mange forskellige implementationer. Det sikrer,
at forskellige komponenter kan udskiftes, uden at det bryder hele systemet. En vigtig egenskab ved AssetAZ-økosystemet
er, at **interfaces overlever, men implementationer kan dø**.
## 4. Interaktion med Nenjim
Nenjim er ansvarlig for at hente, konfigurere og levere instanser af komponenter. AssetAZ spørger blot Nenjim:
“Giv mig en Bitcoin wallet”. Nenjim returnerer en instans, der allerede virker, fordi den er konfigureret med f.eks.
den korrekte private nøgle.
Al konfiguration – f.eks. adgang til eksterne API’er – sker i Nenjim. Plugins i AssetAZ kommunikerer med Nenjim
via lazy-loaded og versionerede komponenter. Det giver mulighed for side-om-side afvikling af gamle og nye versioner
af de samme objekter.
## 5. Observables og Subscribers
Observables er plugins, som udbyder informationer eller events til andre som er interesseret i at modtage den
information. Dem som lytter på informationerne kaldes 'Subscribers'. Når alt går gennem AssetAZ, bliver det muligt at
lave central logging, analyse og f.eks. korrekt skatteberegning (FIFO, m.m.).
Et plugin for agerer Observable, kan også være et system, der registrerer eksterne hændelser (som vandtemperaturen).
Observables kan også abonneres på over P2P – en AssetAZHub kan abonnere på en observable fra en anden hub, og modtage
events som var de lokale.
## 6. AssetAZ-token og incitamenter
AssetAZ-token bruges til at betale udviklere, stemme om beslutninger og give adgang til plugins og services.
Når en opgave skal løses, estimeres prisen i tokens. Udvikleren betales derefter i tokens og kan selv vælge, om de
vil beholde dem som investering eller sælge dem.
Tokens kan også bruges af brugere, der ønsker at få udviklet features, eller som belønning for at bidrage med viden,
kode eller data.
Prisen på en token fastsættes indirekte gennem de “ankre”, som brugere selv sætter – én bruger kan forvente, at én
token svarer til én krone, mens en anden måske mener den er mere værd. Disse forskelle skaber handel og spekulation
på en decentral børs, men kan også sagtens handles “på gaden” (Over the Counter (OTC))
## 7. Peer-to-peer og orakler
Alle AssetAZHubs kører i et P2P-netværk. Det gør det muligt at dele services – fx adgang til wallets, sensordata,
eller plugins – uden at give adgang til selve koden. Man kan sælge orakeltjenester, som f.eks. at måle havets
temperatur hver morgen, og stille det til rådighed som en observable for andre AssetAZHubs.
Det gør det muligt at tjene tokens på andre typer aktiver end kun finansielle – viden og data bliver også til et aktiv.
## 8. Brugeroplevelse
Brugeroplevelsen i AssetAZ afhænger 100% af den terminal og de plugins, man bruger. En AI vil aldrig bruge UI,
mens en menneskelig bruger måske kun vil bruge et smartwatch.
Et plugin kan vælge at gengive information på mange måder – det minder om at sætte et grafikkort i en computer.
Det oversætter data til noget brugeren forstår. Det er op til terminalen at præsentere det.
## 9. Transparens, tillid og signerede builds
Alle plugins er open source. Brugeren kan compile dem selv, eller stole på builds, som andre har udgivet og
kryptografisk signeret.
Brugeren definerer selv en prioriteret liste over hvem de stoler på. Når et plugin skal installeres, prøver Nenjim
at finde et build, der matcher fra én af de godkendte signaturer.
Det er også muligt at tage betaling for signerede builds, fx via AssetAZ-token, men det kræver, at man aktivt
tilføjer et betalingsplugin – Nenjim har ingen afhængighed til AssetAZ medmindre man ønsker det.
## 10. Distribution og installation
For at gøre AssetAZ nemt at installere, kan det leveres som et færdigt image med Lightwale – et minimalistisk,
immutable Linux-system med Docker. Der er kun én container: Nenjim. Den konfigureres til, automatisk, at starte
AssetAZ og de relevante plugins via én konfigurationsfil.
Det gør det muligt at levere en hel hub med alt prækonfigureret, som man bare starter op.
## 11. Fremtidige emner (TODO)
- Backup og gendannelse via IPFS og kryptering
- Sikkerhed og privacy-afsnit
- AI som brugere og agenter
- Governance og fællesskab
- Brugerscenarier og fiktive cases
- Semver-enforcer tool og build-validering
- Videreudvikling af observable-konceptet
- Flydende tillid og signaturudskiftning
- Betalingstjenester for signerede builds
+303
View File
@@ -0,0 +1,303 @@
#<center><H1>Evelyn<br>Documentation</H1></center>
**Indholdsfortegnelse:**
<!-- TOC -->
* [1. Introduktion formål og vision](#1-introduktion-formål-og-vision)
* [1.1 Tokens som vedvarende aktiv – selv efter systemnedbrud](#11-tokens-som-vedvarende-aktiv--selv-efter-systemnedbrud)
* [2. Tokenøkonomi og finansiering](#2-tokenøkonomi-og-finansiering)
* [2.1 Allokering af overskud](#21-allokering-af-overskud)
* [2.2 Buybacks og burn](#22-buybacks-og-burn)
* [2.3 Finansiering af udvikling](#23-finansiering-af-udvikling)
* [2.4 Ansvarsplacering og overskud](#24-ansvarsplacering-og-overskud)
* [2.5 Neutralitet og ligestilling](#25-neutralitet-og-ligestilling)
* [2.6 Vejen mod autonomi](#26-vejen-mod-autonomi)
* [3. Tradingmotorens filosofi - AFETS](#3-tradingmotorens-filosofi---afets)
* [3.1 Ikke-spekulativ tilgang](#31-ikke-spekulativ-tilgang)
* [3.2 Søgning efter ligevægt](#32-søgning-efter-ligevægt)
* [3.3 Anti-fragilitet](#33-anti-fragilitet)
* [4. Trekantmodellen - Longs, Shorts og Margin](#4-trekantmodellen---longs-shorts-og-margin)
* [5. Score-beregning – systemets beslutningsmotor](#5-score-beregning--systemets-beslutningsmotor)
* [5.1 Principper for score-beregning](#51-principper-for-score-beregning)
* [5.2 Beregning af score for åbning af positioner](#52-beregning-af-score-for-åbning-af-positioner)
* [5.3 Straf for store positioner (penalty)](#53-straf-for-store-positioner-penalty)
* [5.4 Beregning af score for lukning af positioner](#54-beregning-af-score-for-lukning-af-positioner)
* [5.5 Fleksibilitet og udvikling](#55-fleksibilitet-og-udvikling)
* [6. Iteration og rebalancering](#6-iteration-og-rebalancering)
* [7. Likviditetsstyring og gearing](#7-likviditetsstyring-og-gearing)
* [7.1 Tærskler og dvale](#71-tærskler-og-dvale)
* [8. Risici, fail-safes og antifragilitet](#8-risici-fail-safes-og-antifragilitet)
* [9. Udvikling, YouTrack-integration og offentlig gennemsigtighed](#9-udvikling-youtrack-integration-og-offentlig-gennemsigtighed)
* [10. Fremtidige emner / TODO](#10-fremtidige-emner--todo)
<!-- TOC -->
## 1. Introduktion formål og vision
Evelyn er et proprietært trading-plugin bygget oven på AssetAZ-platformen. Evelyn er ikke open source og er
ikke designet til at blive kørt af andre. Ingen andre end udvikleren har, på nuværende tidspunkt, adgang til at
bruge, køre eller analysere den. Evelyn eksisterer udelukkende som en privat og beskyttet komponent i et større,
decentraliseret økosystem.
Der arbejdes hen imod, at Evelyn, med tiden, bliver en fuldt autonom enhed med egen kapital og kontrol, hvor ikke
engang udvikleren har adgang til beslutningstagen eller de midlerne – kun mulighed for at interagere
på lige fod med øvrige investorer.
Evelyn er ikke designet til at maksimere profit for ejeren i klassisk forstand. Evelyn er en blackbox, som
eksterne investorer kan interagere med. Al profit, Evelyn genererer gennem bl.a. trading, kanaliseres
tilbage i systemet: til egen drift (hosting, strøm og andre ressourcer), udvikling, polstring imod kriser,
styrkelse af likviditet i trading pools samt tilbagekøb af de 'Evelyn IOU' tokens investorerne har købt.
Den eneste kapital, der tilfalder Evelyn selv, er derfor kun den oprindelige likviditet skabt, da tokens
blev solgt på det åbne marked – det, man kan betragte som hovedstolen på de gældsbeviser, Evelyns IOU tokens
repræsenterer.
Evelyn selv tjener ikke på trading, men bruger trading som et værktøj til at styrke systemet og servicere sine
gældsforpligtelser.
Når Evelyn senere har opbygget en stor egenkapital, vil den anvende en del af denne kapital til filantropisk
støtte i samfundet.
Evelyn distribueres teknisk som enhver anden Nenjim-komponent, men er ikke offentligt registreret, hvilket gør
den praktisk talt usynlig for andre brugere. Denne designbeslutning understøtter Evelyns rolle som en blackbox
til kapitalopbygning og senere filantropisk støtte. Evelyn anvender AFETS-princippet for antifragilitet i sin
tradingstrategi og sikrer løbende systemstabilitet ved at geninvestere profit i likviditetsstyrkelse,
udvikling og opkøb af Evelyn IOU-tokens.
I fremtiden vil eksterne investorer, via tokens, kunne stemme om, hvilke projekter Evelyn skal støtte.
### 1.1 Tokens som vedvarende aktiv – selv efter systemnedbrud
En særlig egenskab ved Evelyns design er, at Evelyn tokens fortsætter med at eksistere og blive handlet på
decentraliserede børser, selv hvis Evelyn selv ophører med at fungere – f.eks. på grund af likvidering af alle
positioner eller markedsmæssige sammenbrud.
Det betyder, at investorers kapital ikke går tabt, selv hvis Evelyn som tradingmaskine fejler. Tokens kan stadig have
værdi og kan handles frit. Det giver mulighed for, at tokens over tid bliver købt op igen – ikke nødvendigvis af
Evelyn, men f.eks. af den oprindelige udvikler med anden indkomst, eller af markedet selv, hvis der opstår ny interesse.
Denne adskillelse mellem token og motor gør, at Evelyn ikke skaber afhængighed, men i stedet definerer en langsigtet
økonomisk kontrakt mellem projekt og investor, som potentielt kan indfries, selv hvis omstændighederne ændrer
sig drastisk.
## 2. Tokenøkonomi og finansiering
Evelyn bruger en intern token kaldet 'Evelyn IOU' som sin primære finansieringsmekanisme. Denne token er en form for
gældbevis, som Evelyn selv udsteder i de tilfælde, hvor der ikke er tilstrækkelig likviditet til at fortsætte
handelsaktiviteterne. Tokens bliver udstedt “ud af den blå luft” og solgt på en decentraliseret børs.
Provenuet fra salget bliver brugt direkte som handelskapital i systemet. Det er vigtigt at forstå, at 'Evelyn IOU'
ikke giver krav på en fast tilbagebetaling – det er en markedsbaseret mekanisme, hvor investorerne forventer at
kunne sælge deres tokens igen til en højere pris, efterhånden som Evelyn opnår overskud.
### 2.1 Allokering af overskud
Når Evelyn genererer overskud gennem sin tradingstrategi, bliver dette overskud automatisk allokeret i forskellige
procentandele til følgende områder:
1) Geninvestering i nye positioner
1) Reduktion af gearing (hvis anvendt)
1) Afbetaling af gæld for tidligere tab
1) Tilførsel af likviditet i Evelyn IOU-tokenets pool
1) Betaling for udvikling og vedligehold, samt drift af systemet
1) Buyback og burn af tokens
Fordelingerne er hardcoded og justeres manuelt af udvikleren, når behovet opstår. Evelyn foretager ikke selv
beslutninger om fordelingen. Det er et lukket kredsløb, hvor alt overskud anvendes til enten at styrke systemet,
sikre tokenens pris eller muliggøre ny funktionalitet.
### 2.2 Buybacks og burn
En af de vigtigste mekanismer i Evelyn er det automatiske tilbagekøb og efterfølgende burn af 'Evelyn IOU' tokens.
En fast procentdel af overskuddet anvendes på at købe tokens på markedet – uanset den aktuelle pris – og brænde
dem permanent. Dette reducerer det samlede udbud og skaber et kontinuerligt deflationært pres, hvilket typisk er
positivt for investorerne. Buyback-strategien sikrer, at investorerne får en god pris for deres tokens, når det
udnyttes at ikke alle investorer sælger på samme tid, da de har forskellige forventninger til prisen.
### 2.3 Finansiering af udvikling
Udvikling i Evelyn sker gennem en task-baseret model. Alle opgaver oprettes i IntelliJ YouTrack og bliver
individuelt prissat i 'Evelyn IOU'. Når en opgave er funded – enten af Evelyn selv (via overskud) eller af en
ekstern sponsor, påbegyndes arbejdet. Når opgaven er markeret som “Done”, udbetales den aftalte mængde tokens
til udvikleren. Alle transaktioner er offentligt synlige via SolScan, hvilket sikrer gennemsigtighed og gør det
muligt at verificere, at en opgave faktisk funded, gennemført og til sidst, betalt for.
Det betyder også, at eksterne investorer kan vælge at støtte specifikke funktioner, hvis de tror på Evelyns fremtid,
selv i perioder hvor Evelyn ikke selv har midler. Det gør tokenen både til en investeringsenhed og et værktøj til at
drive udvikling fremad.
### 2.4 Ansvarsplacering og overskud
Det er vigtigt at understrege, at al profit går til systemet og dets investorer – ikke til ejeren som personlig
gevinst. Den eneste kapital, der kan betragtes som tilhørende ejeren, er den oprindelige likviditet, der blev tilført,
da tokens blev solgt. Evelyn bruger alt efterfølgende overskud til at servicere sin egen gæld, styrke balancen og
understøtte videreudvikling.
### 2.5 Neutralitet og ligestilling
Selvom Evelyn er udviklet og ejet af én person, er selve systemets økonomiske struktur designet til at være neutral.
Hvis ejeren ønsker yderligere gevinst ud over den oprindelige hovedstol fra tokensalget, skal det ske ved at købe
Evelyn IOU-tokens på lige fod med alle andre investorer. Der er ingen bagdør, ingen særbehandling og ingen
skjulte fordele.
Systemet fungerer dermed næsten som et selvejende kredsløb, hvor udvikleren er en deltager på linje med alle andre.
Det er ikke systemets opgave at betale sin ejer – det er systemets opgave at holde balancen, betale sin gæld og
skabe værdi for markedet.
### 2.6 Vejen mod autonomi
Der er en overordnet ambition om, at Evelyn på sigt skal blive et fuldt selvforvaltende system, hvor ikke engang den
oprindelige udvikler har adgang til dens kapital. Det betyder, at den handelskapital, Evelyn genererer eller modtager
via tokensalg, ikke skal kunne trækkes ud af systemet af nogen – heller ikke ejeren.
Idealet er, at Evelyn på et tidspunkt fungerer som en økonomisk organisme med sin egen vilje. En enhed med egne midler,
egne prioriteter og egne beslutninger – hvor adgang til ressourcer kun kan opnås gennem markedet eller gennem faktisk
bidrag til systemet.
Udvikleren bliver i så fald ikke systemets ejer, men blot én deltager blandt andre, der kan tilbyde at løse opgaver
eller investere i tokens. Det er en ny måde at tænke softwareejerskab på: ikke som besiddelse, men som
fællesskab og udveksling.
## 3. Tradingmotorens filosofi - AFETS
Evelyn bygger hele sin handelsstrategi på det, der kaldes AFETS – Anti-Fragile Equilibrium Trading System. Systemet
er konstrueret til ikke blot at tåle markedets svingninger, men at blive stærkere af dem. Det er en strategi, hvor
målet ikke er spekulation eller profitmaksimering, men balancering. Evelyn forsøger ikke at forudsige fremtiden.
Den handler udelukkende på, hvad der er sandt lige nu.
### 3.1 Ikke-spekulativ tilgang
I modsætning til traditionelle tradingbots, der forsøger at gætte fremtidige prisbevægelser ud fra mønstre, nyheder
eller tekniske indikatorer, arbejder Evelyn retrospektivt. Den bygger sine beslutninger på observerede forhold
i nuet – og aldrig på spekulation. Det gør den ekstremt robust over for chok, kriser og nyhedsdrevne spikes, fordi
den ikke lader sig rive med af narrativer, der måske viser sig forkerte.
### 3.2 Søgning efter ligevægt
Hele grundideen i AFETS er at bevare en økonomisk ligevægt. Det betyder, at Evelyn hellere vil være stabil end
aggressiv. Den ser det som en styrke at overleve, ikke at vinde stort. Det gør systemet egnet til lang levetid
og kontinuerlig forbedring – og til at undgå store tab.
### 3.3 Anti-fragilitet
Konceptet om antifragilitet kommer fra Nassim Nicholas Taleb og beskriver systemer, der ikke bare er robuste,
men som faktisk vokser af kaos og stress. Evelyn søger netop dette: at komme styrket ud af udsving i markedet.
Når en ubalance opstår, bruger Evelyn muligheden for at lukke positioner med profit og styrke sin margin.
Når tingene vender, er den klar til at tage det næste sving med endnu større stabilitet.
Evelyn vinder altså ikke ved at være den hurtigste eller smarteste, men ved at være den mest tålmodige og
bedst balancerede. Det er ikke et system, der skal slå markedet – det er et system, der skal overleve og
tilpasse sig det.
## 4. Trekantmodellen - Longs, Shorts og Margin
Evelyn opererer med en unik model, hvor dens samlede kapital opdeles i tre hovedkomponenter: long-positioner,
short-positioner, og available margin. Disse tre komponenter udgør tilsammen en trekant, hvor hver side repræsenterer
en tredjedel af den samlede værdi. Målet er, at hver af balance imellem de tre dele altid tilstrebes, så hver side
af trekanten udgør cirka 33% af den samlede kapital.
Long-positionerne repræsenterer de handler, hvor Evelyn satser på, at værdien af et givent aktiv vil stige.
Short-positionerne er de handler, hvor Evelyn forventer, at værdien vil falde.
Available margin er den likvide kapital, som ikke er bundet i positioner, og som er klar til at blive brugt til nye
handler eller til at genoprette balance, hvis det bliver nødvendigt.
Når der opstår ubalance i trekanten, for eksempel hvis long-positionerne stiger i værdi og dermed fylder mere
end 33% af den samlede kapital, vil Evelyn iværksætte en rebalancering. Det gør den ved at lukke en del af de
kontrakter fra en af de overrepræsenterede positioner og føre værdien tilbage til 'available margin'.
I næste iteration kan Evelyn så vurdere, om available margin nu er for høj og derved bruge noget af den til at åbne
nye short-positioner for at genoprette balancen. Målet er altid at bringe alle tre komponenter tilbage til en
ligelig fordeling.
Når short-positionerne stiger i værdi (og derved er i tab), kan Evelyn stå i en situation, hvor den har svært ved at
lukke disse positioner uden at forværre ubalancen. For at håndtere dette, vil Evelyn typisk skiftevis lukke en long
og en short i en form for “tiptoe”-mønster. Denne metode gør det muligt for Evelyn at balancere sine positioner over
tid, uden at skabe pludselige, store udsving i systemet.
## 5. Score-beregning – systemets beslutningsmotor
Score-beregningen er hjertet i Evelyns beslutningsproces. Når Evelyn skal vælge, hvilke positioner der skal åbnes
eller lukkes, bruger den en scorefunktion til at vurdere alle muligheder objektivt og konsekvent. Formålet er at
vælge den position, der skaber mest balance og bedst forbedrer porteføljens risiko/profit-forhold.
### 5.1 Principper for score-beregning
- Evelyn vurderer altid alle kandidater (f.eks. alle mulige nye kontrakter, eller alle åbne positioner, der kan lukkes).
- For åbning af positioner bruges både markedsdata (pris, kontraktstørrelse, gearing mv.) og et strafsystem, så ingen
positioner bliver for store i forhold til de andre.
- For lukning af positioner er scoren pt. simplere (procentuel gevinst/tab), men kan udvikles videre i fremtiden.
### 5.2 Beregning af score for åbning af positioner
Når en ny kontrakt skal vælges, samles alle relevante input i en record (se kode-eksempel i appendiks). Beregningen
vurderer blandt andet:
- Hvilken profitforbedring opnås, hvis prisen udvikler sig fra nuværende til “efter trade”-niveau?
- Hvor stor er profitten i forhold til den kapital, der bruges?
- Hvor meget spredes risikoen ved at vælge denne position frem for at forstørre eksisterende?
Eksempel på scoring:
- Beregn forskel mellem nuværende og fremtidig pris for aktivet (alt efter om det er en long eller short)
- Udregn profit for denne prisændring
- Relativ profit (profit pr. dollar brugt)
- Summér til en endelig score: score = 1 + (relativ profit)
### 5.3 Straf for store positioner (penalty)
For at undgå, at Evelyn bygger uforholdsmæssigt store positioner i ét aktiv, justeres scoren nedad for de kandidater,
hvor den nuværende position allerede er større end gennemsnittet. Straf-funktionen dæmper scoren med en faktor,
der vokser med afvigelsen fra gennemsnittet:
- Hvis positionen > gennemsnittet, så:
- adjustedScore = rawScore / (1 + K * (ratio-1)^2)
Her er K en justerbar parameter, der bestemmer, hvor hårdt store positioner straffes.
### 5.4 Beregning af score for lukning af positioner
For at lukke positioner anvender Evelyn pt. en simpel score baseret på den procentuelle gevinst eller tab,
positionen har. Listen af potentielt lukkede positioner sorteres på denne score, og den bedste vælges.
### 5.5 Fleksibilitet og udvikling
Alle scorefunktioner er implementeret, så de kan udskiftes, tunes og forbedres uden at bryde resten af systemet.
Det gør det let at eksperimentere med nye logikker og justere for mere optimal risikospredning eller
profitforbedring over tid.
## 6. Iteration og rebalancering
Evelyn arbejder i faste iterationer, hvor den med jævne mellemrum – for eksempel hver time – evaluerer sine
positioner. I hver iteration summerer Evelyn værdien af alle longs, alle shorts og den available margin for
at sikre, at de tre komponenter er i balance. Hvis for eksempel long-positionerne bliver for store i forhold
til de to andre dele, vil Evelyn lukke nogle af disse positioner og føre værdien tilbage til den available
margin. Dette sikrer, at der ikke opstår en skævvridning, som kan true systemets stabilitet.
Rebalanceringen foregår altid med forsigtighed for at undgå overreaktioner, og justeres i små skridt, så systemet
altid er i stand til at absorbere markedsudsving uden at tabe balancen.
## 7. Likviditetsstyring og gearing
Evelyns likviditetsstyring er bygget op omkring klare tærskler og trin:
- Normalt drift: Systemet forsøger at holde available margin på ca. 33%. Det sker ved balancering mellem long-,
short-positioner og likvide midler.
- Faldende tilgængeling margin: Hvis available margin falder under et defineret threshold (fx 10%), begynder Evelyn
at øge gearingen på eksisterende positioner. Det frigør kapital, men bruges kun til at holde margin på minimum,
ikke til at tage flere handler.
- Kritisk lav margin: Hvis gearing ikke kan øges yderligere, og marginen rammer 0%, vil Evelyn udstede nye
'Evelyn IOU' tokens som sidste udvej for at skaffe ekstern likviditet.
Når markedet normaliseres, nedbringer Evelyn langsomt gearingen over tid, finansieret af trading-overskud. Det
betyder, at systemet efter en krise ikke straks går tilbage til spot-only, men gradvist heler balancen, lidt som
et brækket ben, der får tid til at blive stærkt igen. Under hele processen prioriteres stabilitet over aggressivitet.
### 7.1 Tærskler og dvale
Hvis ubalancen bliver for ekstrem – eksempelvis hvis short-siden eller gearing dominerer – kan Evelyn gå i
dvale (pause al trading). Dette sikrer, at systemet ikke vælter, men tålmodigt venter på bedre markedsforhold
eller ny kapitaltilførsel.
## 8. Risici, fail-safes og antifragilitet
Evelyn er designet til at overleve og vokse af markedsstress, ikke blot at modstå det. Systemet har flere
indbyggede mekanismer for at sikre antifragilitet:
- Fail-safe-mode: Hvis balancen mellem longs, shorts og margin bliver for skæv (fx hvis short-siden vokser
for meget), kan Evelyn sætte sig selv på pause og stoppe al trading. Det sikrer, at systemet aldrig tvinges ud i
ruinøse handler, men hellere venter tålmodigt på, at markedet vender, eller på at der tilføres ny margin.
- Tærskelbaseret gearing og tokensalg: Evelyn bruger gearing kun som midlertidigt værktøj, og kun til at holde
marginen lige over det kritiske minimum. Kun hvis dette fejler, udstedes nye tokens.
- Tålmodighed over panik: Systemet undgår bevidst at tage hurtige, risikable beslutninger under stress – i stedet
nedbringer det gearing og bygger langsomt balancen op igen.
- Antifragilitet: Når Evelyn oplever markedskaos, bliver den stærkere, fordi dens strategi bygger på at udnytte
ubalancer og reinvestere gevinsterne i at reparere systemet.
## 9. Udvikling, YouTrack-integration og offentlig gennemsigtighed
Evelyns udviklingsmodel bygger på transparens og dokumenterede processer, selvom selve koden ikke er offentlig:
- YouTrack-integration: Alle udviklingsopgaver oprettes og prissættes i EVELYN-tokens i YouTrack. Når en opgave er
funded (af Evelyn selv eller af eksterne), igangsættes arbejdet. Når den er færdig, udbetales tokens til udvikleren,
og transaktionen logges med ID på SolScan.
- Offentlig revision: Enhver kan følge med i, hvilke opgaver der er funded, hvem der har modtaget betaling, og
hvilken transaktion der ligger bag. Dermed er der fuld gennemsigtighed i systemets økonomi uden at afsløre den
underliggende strategi.
- Funding af opgaver: Opgaver bliver kun udført, hvis de er funded. Investorer og udviklere kan selv gå ind og
sponsorere opgaver, hvis de tror på systemet.
## 10. Fremtidige emner / TODO
- Bedre adgangsbegrænsning: Overvej, hvordan private plugins i Nenjim kan beskyttes bedre end “security by obscurity”.
- Automatisk tuning af allokeringsprocenter: Undersøg muligheder for at lade systemet lære og tilpasse sin interne
kapitalallokering over tid (AI-drevet tuning).
- Flere tradingstrategier: Udvid Evelyn med mulighed for at tilføje alternative, konkurrerende scoring-algoritmer.
- Bedre penalty-funktioner ved close: Implementer og test mere avanceret straflogik for lukning af positioner.
- Ekstra fail-safes: Tilføj flere automatiske stopmekanismer for at undgå at systemet “vipper omkuld” i
ekstreme markedsbevægelser.
- Open API for ekstern integration: Gør det muligt at tilføje eksterne datafeeds, udveksle signaler med andre
systemer, eller automatisere funding-opgaver.
+211
View File
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nenjim Documentation</title>
<style>
body { font-family: Arial, sans-serif; background: #f8f9fa; color: #263238; margin: 0; }
main { max-width: 980px; margin: auto; padding: 2rem; background: white; line-height: 1.55; }
h1, h2, h3 { color: #0056b3; }
code, pre { background: #f1f3f5; }
code { padding: .1rem .25rem; }
pre { padding: 1rem; overflow-x: auto; border-left: 4px solid #0056b3; }
.status { padding: 1rem; background: #e7f1ff; border-left: 4px solid #0056b3; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd4da; padding: .55rem; text-align: left; vertical-align: top; }
</style>
</head>
<body>
<main>
<h1>Nenjim</h1>
<p class="status"><strong>Implementation status:</strong> Journal format version 1, the immutable Java model,
internal strict parser, read-only <code>JournalService</code> with a Hub-managed process implementation,
passive explicitly refreshed <code>JournalServiceManager</code>, Hub-managed
<code>JavaFxRuntimeService</code>, the first central component Registry, and mandatory Hub configuration
format 1 are implemented. Contexts, resolution, classloading, discovery, runtime loading/removal,
downloading, publishing, synchronization, watching, signatures, permissions, and license filtering remain
future work.</p>
<h2>One Journal per artifact</h2>
<p>A Journal describes exactly one artifact. API, test, and implementation artifacts therefore have independent
Journals and chains. Roles such as API or implementation are not stored as a <code>TYPE</code>.</p>
<pre><code>&lt;GROUP&gt;-&lt;MODULE&gt;-&lt;ARTIFACT&gt;
com_r35157_nenjim-hubd-api
com_r35157_nenjim-hubd-api:1.0.3</code></pre>
<p>The three metadata components identify the chain; there is no separate <code>JOURNAL_ID</code>.</p>
<h2>Complete immutable snapshots and history</h2>
<p>Every Journal revision is a complete worldview of the artifact and all releases known in that revision. It is
not a delta and inherits nothing implicitly. A later snapshot may change metadata, add or remove releases,
correct release knowledge, alter dependencies or policy, or be a semantic no-op.</p>
<p>The genesis omits <code>PREVIOUS_JOURNAL_DIGEST</code>. Each successor names the SHA-256 digest of the
predecessor's exact UTF-8 text, including whitespace and line endings. A chain has one genesis and one head;
missing predecessors, forks, cross-artifact links, and non-increasing Journal versions are invalid.</p>
<p><code>JOURNAL_VERSION</code> is when the worldview was published. <code>PUBLISHED_AT</code> is when an artifact
release was published. Both use <code>uuuuMMddHHmmssSSS'Z'</code>, but historical knowledge is selected from
the Journal chain rather than by filtering the newest snapshot on release publication time.</p>
<h2>Metadata and release content</h2>
<p><code>DESCRIPTION</code> is mandatory single-quoted text. <code>INFORMATION_URI</code> is optional single-quoted
metadata and must decode to a non-empty absolute URI. Format 1 does not restrict schemes: HTTPS, FTP, IPFS,
and other syntactically valid schemes are accepted without opening or interpreting their content.</p>
<p>Every exact <code>major.minor.patch</code> release has a mandatory single-quoted <code>LICENSE</code>, a
publication timestamp, non-negative byte size, and one or both supported content identities:
<code>sha256</code> and CIDv1 (<code>cid1</code>). License text is preserved as free text; no vocabulary,
compatibility decision, or filtering is applied.</p>
<h2>Version expressions</h2>
<table>
<thead><tr><th>Expression</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>1.0.3</code>, <code>[1.0.3]</code></td><td>The same exact release.</td></tr>
<tr><td><code>[1.0]</code></td><td>Every stable 1.0.x release; not exact 1.0.0.</td></tr>
<tr><td><code>[1.0.3--&gt;1.0.7]</code></td><td>A closed range including both exact endpoints.</td></tr>
<tr><td><code>[1.0--&gt;1.2)</code></td><td>Includes 1.0.x and 1.1.x, excluding the complete 1.2.x series.</td></tr>
<tr><td><code>[1.0--&gt;</code></td><td>Starts at 1.0.0 and stops before 2.0.0.</td></tr>
</tbody>
</table>
<p>Comma-separated expressions form a union. Boundaries require at least major and minor. Mathematical
comparison syntax, incomplete bare exact versions, prereleases, build metadata, empty elements, and reversed
or empty ranges are rejected.</p>
<h2>Dependencies</h2>
<p>Dependencies belong to individual releases and always have an explicit scheme. Format 1 implements only the
<code>nenjim:</code> scheme; Maven resolution and unknown schemes are not silently accepted.</p>
<pre><code>DEPENDS_ON=nenjim:com_r35157_nenjim-hubd-api=[1.0--&gt;
EXCLUDES=nenjim:com_r35157_nenjim-hubd-api=1.0.5:'Known incompatibility'
PREFERRED=nenjim:com_r35157_nenjim-hubd-api=1.0.3:'Built together'</code></pre>
<p><code>DEPENDS_ON</code> is the hard base set. <code>EXCLUDES</code> removes hard-invalid combinations for that
relationship. <code>PREFERRED</code> is a hint and must remain a subset of
<code>DEPENDS_ON - EXCLUDES</code>. Repeated rules form unions while each line and description remains
inspectable. The Journal layer represents these rules but does not resolve a graph.</p>
<h2>Artifact policy</h2>
<ul>
<li><code>BLACKLIST</code> is a hard global prohibition.</li>
<li><code>DISCOURAGED</code> is a negative hint; a matching release remains selectable.</li>
<li><code>RECOMMENDED</code> is a positive hint naming one exact release in the same snapshot.</li>
</ul>
<p>Overlaps are valid and hard rules win over hints. A future resolver should seek a valid graph containing as
many compatible recommendations as possible. The highest valid version is a fallback, not an unconditional
rule, so future convergence may move from a higher local version to a lower recommended version.</p>
<h2>Strict parsing and immutable model</h2>
<p><code>FORMAT_VERSION=1</code> must be the first actual entry. Blank lines and comments beginning with
<code>#</code> are accepted, while a hash inside single quotes is data. Quoted values support escaped quotes
(<code>\'</code>) and backslashes (<code>\\</code>). Unknown, malformed, duplicate, misplaced, empty, or
semantically inconsistent content fails with source name, one-based line number, and a reason. Validation is
complete before service state changes. Returned objects and collections are immutable and defensively copied.</p>
<h2>Service, process, ServiceManager, packages, and disk layout</h2>
<p>A <code>Service</code> is the public domain API used by applications and other services during ordinary
operation. <code>JournalService</code> is a registrable component but remains filesystem-independent and
read-only: it exposes current and historical Journal queries without lifecycle, paths, refresh, parser, or
mutation operations. Queries become available only after successful Journal process startup.</p>
<p>The Hub constructs one Journal object exposed as both <code>JournalService</code> and
<code>NenjimProcess</code>, plus one distinct passive <code>JournalServiceManager</code>. The manager reports
the normalized data root and performs explicit refresh; it has no start, stop, service accessor, component
identity, or process lifecycle. It delegates through a package-private administration view that ordinary
service consumers cannot name or derive.</p>
<p>The Journal process reads UTF-8 runtime files chronologically and validates directory and filename placement.
Its complete initial worldview is published only after every runtime file succeeds; failed startup leaves
queries unavailable and cannot be retried on the same instance. Later refresh remains explicit, idempotent,
and atomic per ingestion. Neither startup nor refresh creates, overwrites, or deletes Journal files.</p>
<p>The package-private <code>JournalServiceImpl</code> keeps exact-text SHA-256 identity, chain state,
idempotency, conflicts, forks, and ingestion internal. The public package contains only the two central
interfaces; composed models, value types, and exceptions live under <code>.model</code>,
<code>.valuetypes</code>, and <code>.exception</code>, while parser, ingestion, chain state, filesystem
loading, and the admin boundary live under <code>.impl.ref</code> without leaking through public
signatures.</p>
<p>All modern JavaFX applications share one JVM-wide toolkit. When selected, the Hub constructs one
<code>JavaFxRuntimeServiceImpl</code>, registers it as <code>nenjim.javafx-runtime.service</code>, starts it
through its <code>NenjimProcess</code> view, and injects its lifecycle-free service view into later selected
JavaFX applications. Only service startup calls <code>Platform.startup(...)</code>; task submission never
initializes or retries JavaFX and is rejected until startup succeeds. A headless process plan that omits all
JavaFX processes does not construct the runtime or initialize the toolkit.</p>
<pre><code>data/nenjim/journals/
└── &lt;artifact-coordinate&gt;/
├── &lt;journal-version&gt;.journal
└── &lt;journal-version&gt;.journal.example</code></pre>
<p>Only <code>.journal</code> files are runtime input. Version-controlled <code>.journal.example</code> files are
documentation and are ignored by the service.</p>
<h2>Components and the central Registry</h2>
<p>A Nenjim component is a fully constructed building block ready for use. Its consumer decides whether it acts
as a service, application, algorithm, or adapter. A service exposes a domain API, while application describes
a user-facing or orchestration role. <code>NenjimComponent</code> is therefore a pure marker. Independently,
the canonical <code>com.r35157.nenjim.process.NenjimProcess</code> marks an active component that the Hub can
start and track; it adds only <code>start()</code> and no common stop, restart, or health operation.</p>
<p>A <code>NenjimComponentId</code> is lower-case lookup metadata owned by one Registry, not a property of the
object, an authorization boundary, or an artifact version. One object may have several IDs. Conversely, one
registration is indexed under every component interface implemented by that object, so the Registry object's
typed service and process views retain reference identity.</p>
<p><code>NenjimRegistryService</code> is the injected read-only consumer view. It returns immutable,
registration-ordered ID snapshots for a component interface and supports lookup by explicit ID plus expected
interface. Missing IDs return <code>null</code>; requesting an existing ID through the wrong interface is a
caller error. Registration is available only through a package-private administration view behind the passive
Registry manager. That manager exposes only registration, is neither a component nor a process, is not
registered, and cannot start anything. The internal administration interface must remain package-private so
an ordinary service consumer cannot cast the Registry object to a mutation view.</p>
<h3>Mandatory NenjimHub.conf format 1</h3>
<p><code>NenjimHubImpl</code> is the global composition and runtime owner. Before any process starts it reads only
the readable regular file <code>conf/NenjimHub.conf</code>. The version-controlled
<code>conf/NenjimHub.conf.example</code> is a copy-only template: the application never reads, copies,
generates, or falls back to it or to a hardcoded startup sequence.</p>
<pre><code>FORMAT_VERSION=1
[AUTO_START]
nenjim.registry.service
nenjim.journal.service</code></pre>
<p><code>FORMAT_VERSION=1</code> must be the first actual entry and the one exact
<code>[AUTO_START]</code> header must be the next actual entry. Leading and trailing whitespace is ignored.
A trimmed line beginning with <code>#</code> is a comment, and <code>#</code> after content begins a trailing
comment. Inside the section a comment-only line does not terminate it, while the first whitespace-only line
or end of file does. After termination only blank lines and comments are permitted; there is no quoting or
escaping for <code>#</code>.</p>
<p>Every active section line is a canonical component ID. The complete list is validated before Registry starts:
duplicates, invalid or unknown IDs, passive-only IDs, and IDs the Hub cannot construct as modern processes
fail. Registry must occur exactly once first and Journal exactly once second. The known selectable processes
are Registry, Journal, Ticker, Jupiter Perps Alarm, both Evelyn services, Production IOU Burner, JavaFX
runtime, Mission Control, and Process Manager. The version-controlled template selects only the mandatory
Registry and Journal processes; every other process must be added explicitly to the private deployment
configuration.</p>
<p>After validation the Hub starts, registers, and records Registry, then eagerly constructs and registers the
complete passive graph without activating a process. It next visits every remaining configured ID in exact
order, beginning with Journal. Each selected process is constructed only when reached, registered before
<code>start()</code>, and recorded only after successful return. An omitted process is therefore neither
constructed, registered, started, nor displayed by Process Manager.</p>
<p><code>[AUTO_START]</code> is the complete manually ordered process dependency closure. The Hub does not infer,
insert, recursively construct, or reorder dependencies; JavaFX runtime must precede selected JavaFX
applications. Construction, registration, and startup remain fail-fast without rollback. Earlier successful
snapshot entries remain truthful after a later failure, and the online point plus blocking wait are reached
only after every configured process succeeds.</p>
<p>Ordinary consumers receive the lifecycle-free <code>TickerService</code> view, which exposes only the
latest-price query. The same <code>TickerServiceImpl</code> object separately implements
<code>NenjimProcess</code> so the Hub owns startup. The implementation starts its configured active
<code>PriceSource</code> instances internally and stops those already started if Ticker startup fails; it
exposes no general Ticker stop operation.</p>
<p><code>EvelynIOUBurnerService</code> follows the same separation even though its current domain-service view is
empty. It supplies typed Registry and dependency identity without external controls, while the same
<code>EvelynIOUBurnerServiceImpl</code> object separately implements <code>NenjimProcess</code> for Hub-owned
startup. Neither the service interface nor the implementation exposes an ordinary stop or restart operation.</p>
<p>The Ticker still explicitly selects the Raydium EVE/USDT source ID while the hardcoded source remains a passive,
registered component. Process Manager continues to poll immutable ordered snapshots of actual successful
startup milestones rather than displaying configured intent or claiming current health.</p>
<p><code>com.r35157.nenjim.hubd.Main</code> remains only the outer Java bootstrap. It constructs the reference Hub
through the public <code>NenjimHub</code> interface and calls <code>start()</code> once. The Hub is neither a
component nor a process and is not registered inside its own Registry.</p>
<p>The Registry remains internally open to later registration, but this version has one catalogue and no Context,
version resolution, classloader integration, discovery, public runtime loading or registration, removal,
persistence, events, ownership tracking, usage counts, permissions, scopes, filtered views, automatic
dependency construction, or automatic start-all behavior.</p>
<h2>Future Nenjim vision</h2>
<p>Contexts, dependency-graph resolution, recursive process construction, automatic selection/reconciliation,
runtime upgrade or downgrade, classloaders, artifact downloads, IPFS retrieval, Maven dependencies,
signatures, publishing, remote synchronization, filesystem watching, and UI/CLI management are deliberately
outside this implementation.</p>
</main>
</body>
</html>
+509
View File
@@ -0,0 +1,509 @@
#<center><H1>Nenjim<br>Documentation</H1></center>
> **Dokumentstatus:** Journal-format version 1, den immutable Java-model, den interne tekstparser, det read-only
> `JournalService` med en Hub-styret procesimplementation, den passive eksplicit refresh-baserede
> `JournalServiceManager`, den Hub-styrede `JavaFxRuntimeService`, første version af det centrale component Registry
> og den obligatoriske format 1 Hub-konfiguration er implementeret. Contexts, resolution, discovery, runtime loading
> og removal er fortsat fremtidig funktionalitet.
**Indholdsfortegnelse:**
<!-- TOC -->
* [1. Introduktion til Nenjim](#1-introduktion-til-nenjim)
* [2. Versionering og Afhængigheder](#2-versionering-og-afhængigheder)
* [3. Opdateringer og Sikkerhed](#3-opdateringer-og-sikkerhed)
* [4. Betalingssystem og Integration med AssetAZ](#4-betalingssystem-og-integration-med-assetaz)
* [5. Backing Store og Fleksibilitet](#5-backing-store-og-fleksibilitet)
* [6. Standardiseret og Automatiseret Pakkehåndtering](#6-standardiseret-og-automatiseret-pakkehåndtering)
* [7. Betydningen af Semantisk Versionering](#7-betydningen-af-semantisk-versionering)
* [8. Componenter og det centrale Registry](#8-componenter-og-det-centrale-registry)
* [9. Integration med AssetAZ og økonomisk aktivitet](#9-integration-med-assetaz-og-økonomisk-aktivitet)
<!-- TOC -->
## 1. Introduktion til Nenjim
Nenjim skal på sigt kunne håndtere forskellige versioner af softwareartifakter samtidigt. Det nu implementerede
fundament er Journal-laget: et strengt tekstformat og en immutable model, som kan fastholde, hvad der var kendt om
en artifakt på et bestemt tidspunkt. Det første Registry kan desuden katalogisere de konstruerede componenter, og
Hub-konfigurationen vælger den manuelt ordnede procesmængde. Dependency-resolution, Contexts, downloading og
classloading kommer senere.
## 2. Versionering og Afhængigheder
Nenjim beskriver moduler, artifakter, releases, relationer og versionspolitik i Journaler. Almindelig Java-kode skal
ikke indbygge Nenjim- eller versionsspecifikke navne for at bruge et API. Et Context beskriver de ønskede valg, og
Nenjim resolver senere disse valg og Journalernes metadata til en eksakt artifakt- og dependency-graf.
### 2.1 Én Journal per artifakt
En Journal beskriver præcis én artifakt. API, tests og referenceimplementation har derfor hver sin Journal; roller
som `API`, `IMPLEMENTATION` og `TEST` gemmes ikke som et `TYPE`-felt. Journalens identitet udledes af metadata:
```text
<GROUP>-<MODULE>-<ARTIFACT>
```
Eksempelkoordinater er `com_r35157_nenjim-hubd-api`, `com_r35157_nenjim-hubd-tests` og
`com_r35157_nenjim-hubd-impl_ref`. En komplet release-reference tilføjer en eksakt Semantic Version:
```text
com_r35157_nenjim-hubd-api:1.0.3
```
Der findes intet separat `JOURNAL_ID`.
### 2.2 Metadata og release-indhold
`[METADATA]` indeholder coordinate-delene og en obligatorisk single-quoted `DESCRIPTION`. Den valgfrie
`INFORMATION_URI` er også single-quoted, skal være en syntaktisk gyldig absolut `URI`, og kan bruge enhver scheme,
for eksempel `https:`, `ftp:` eller `ipfs:`. Nenjim åbner eller fortolker ikke URI'en automatisk.
Hver `[RELEASE major.minor.patch]` indeholder sin egen single-quoted `LICENSE`, `PUBLISHED_AT`, `CONTENT_SIZE` og
mindst én `CONTENT_DIGEST`. Format 1 accepterer valideret `sha256` og CIDv1 (`cid1`). Licensen er fri tekst, gemmes
uden normalisering og kan være forskellig mellem releases; fremtidig licensfiltrering er ikke implementeret.
### 2.3 Komplette snapshots og Journal-kæden
Hver Journal-fil er et komplet immutable snapshot af al aktuel viden om artifakten og dens kendte releases. En
revision er aldrig en delta og arver intet implicit. En nyere revision kan ændre metadata, policy, dependencies og
selv korrigere eller fjerne tidligere kendt release-information. Et no-op snapshot er også gyldigt.
`JOURNAL_VERSION` er et strengt stigende UTC-timestamp med millisekundpræcision:
```text
uuuuMMddHHmmssSSS'Z'
```
Genesis-revisionen udelader `PREVIOUS_JOURNAL_DIGEST`. Hver efterfølger refererer til SHA-256 over forgængerens
komplette UTF-8-tekst præcis som leveret, inklusive whitespace og linjeskift. Manglende forgængere, forks, flere
genesis-revisioner, fremmede artifaktkæder og ikke-stigende versioner afvises.
`PUBLISHED_AT` er release-tid og er uafhængig af Journalens publiceringstid. Historiske queries vælger et helt
snapshot ud fra Journal-kæden; de filtrerer ikke det nyeste snapshot efter `PUBLISHED_AT`.
### 2.4 Version expressions
En `SemanticVersion` (`1.0.3`) er én eksakt release. En `VersionExpression` beskriver et sæt acceptable releases:
* `1.0.3` og `[1.0.3]` er samme eksakte selector.
* `[1.0]` betyder hele den stabile `1.0.x`-serie, ikke eksakt `1.0.0`.
* `[1.0.3-->1.0.7]` inkluderer begge grænser; `)` ekskluderer øvre grænse.
* `[1.0-->1.2]` inkluderer hele `1.2.x`; `[1.0-->1.2)` gør ikke.
* `[1.0-->` stopper før `2.0.0`; `(1.0-->` starter ved `1.1.0`.
* Komma adskiller en union af expressions.
Endpoints skal mindst indeholde major og minor. Matematiske operatorer som `>=`, prereleases, build-metadata,
tomme elementer, omvendte ranges og implicitte cross-major open ranges afvises.
### 2.5 Dependencies
Dependencies hører til en bestemt artifaktrelease og bruger altid en explicit scheme. Format 1 implementerer kun:
```text
DEPENDS_ON=nenjim:com_r35157_nenjim-hubd-api=[1.0-->
EXCLUDES=nenjim:com_r35157_nenjim-hubd-api=1.0.5:'Known incompatibility'
PREFERRED=nenjim:com_r35157_nenjim-hubd-api=1.0.3:'Built together'
```
`DEPENDS_ON` er basissættet. `EXCLUDES` er en hård relationel begrænsning. `PREFERRED` er kun et hint og skal være
en delmængde af `DEPENDS_ON - EXCLUDES`. Flere exclusion/preference-linjer for samme target kombineres som union,
men deres individuelle beskrivelser bevares. Ukendte schemes, herunder `maven:`, afvises i format 1.
### 2.6 Artifact policy
Den valgfrie `[POLICY]` bevarer individuelle regler og beskrivelser:
* `BLACKLIST` er en hård global regel: matchende releases må ikke vælges.
* `DISCOURAGED` er et negativt hint: releasen kan stadig vælges.
* `RECOMMENDED` er et positivt hint til én eksakt release, som findes i samme snapshot.
Overlap er tilladt, og hårde regler vinder over hints. En fremtidig resolver bør søge en gyldig graf med flest muligt
kompatible anbefalinger; højeste versionsnummer er fallback, ikke en ubetinget regel. Det kan senere føre til
konvergens fra en højere lokal release til en lavere anbefalet release, men den adfærd er ikke implementeret nu.
### 2.7 Parser og immutable model
Parseren kræver `FORMAT_VERSION=1` som første faktiske entry, men tillader blanke linjer og `#`-kommentarer.
`#` inde i single quotes er tekst. Quoted værdier understøtter `\'` og `\\`. Ukendte, malformed, duplikerede eller
forkert placerede felter/sektioner afvises med source, linjenummer og årsag. Hele dokumentet valideres før servicens
state ændres. Alle publicerede modeller og collections er immutable og defensivt kopieret.
### 2.8 JournalService, JournalServiceManager og disk-layout
En `Service` er det domæne-API, som applikationer og andre services bruger i daglig drift. `JournalService` er en
registrerbar Nenjim component, men API'et er fortsat filesystem-uafhængigt og read-only: det kan vise coordinates,
kronologiske revisionslister, latest, exact og newest-at-or-before snapshots samt en eksakt release fra et valgt
snapshot. Det eksponerer ikke lifecycle, filesystem paths, refresh, parser eller mutation. Queries er først
tilgængelige, når Journal-processens initiale loading er lykkedes.
En `ServiceManager` er et separat administrationshandle, som en composition/lifecycle owner kan beholde uden at
give almindelige service-consumers administrative rettigheder. `JournalServiceManager` er passiv og eksponerer kun
den normaliserede Journal-root og explicit refresh; den har ingen `start()`, `stop()` eller service-accessor og er
hverken component eller process. Hub'en konstruerer ét Journal-objekt, som både er `JournalService` og
`NenjimProcess`, samt en separat manager bundet til objektets package-private admin-view.
Når Hub'en starter Journal-processen, læser referenceimplementationen komplette UTF-8-filer kronologisk og
validerer placement/navne. Hele den initiale worldview publiceres først, når alle runtime-filer er accepteret; en
fejl efterlader servicen utilgængelig uden partial state og samme instans kan ikke startes igen. Refresh efter
vellykket startup er fortsat explicit, idempotent og per-ingestion atomisk; der er ingen watcher eller background
polling.
`JournalServiceImpl` vedligeholder internt kæderne og udfører atomisk ingestion med exact-text SHA-256. Identisk tekst
er idempotent; anderledes tekst med samme coordinate og Journal-version er en conflict. Parser, ingestion og mutable
chain state er implementation details og findes ikke i public signatures. Public API-pakkerne er opdelt således:
```text
com.r35157.nenjim.service.journal JournalService, JournalServiceManager
com.r35157.nenjim.service.journal.model sammensatte immutable domænemodeller
com.r35157.nenjim.service.journal.valuetypes små værdidefinerede typer
com.r35157.nenjim.service.journal.exception Journal-specifikke exceptions
com.r35157.nenjim.service.journal.impl.ref referenceimplementation og interne helpers
```
```text
data/nenjim/journals/
└── <artifact-coordinate>/
├── <journal-version>.journal
└── <journal-version>.journal.example
```
Kun `.journal` er runtime-input. `.journal.example` dokumenterer formatet og indlæses ikke. Roden skal allerede
eksistere og være læsbar; Journal-processen og dens manager opretter, overskriver eller sletter ikke Journal-filer.
## 3. Opdateringer og Sikkerhed
Det implementerede Journal-lag kan repræsentere nye sikkerhedsoplysninger som en nyere immutable revision, for
eksempel via `BLACKLIST` eller en ændret `RECOMMENDED`. Automatisk synchronization, selection, reconciliation og
upgrade/downgrade er fremtidigt arbejde.
## 4. Betalingssystem og Integration med AssetAZ
Betaling og AssetAZ-integration er langsigtet vision og er ikke en del af Journal-formatet eller denne implementation.
## 5. Backing Store og Fleksibilitet
Den nuværende Journal-proces læser kun det dokumenterede lokale disk-layout gennem en intern filesystem-loader.
Artifact downloading, IPFS retrieval, remote Journals og andre backing stores er ikke implementeret. En
`ipfs:`-værdi i `INFORMATION_URI` er metadata, ikke en retrieval-instruks.
## 6. Standardiseret og Automatiseret Pakkehåndtering
Journalen modellerer dependency-krav og indholdsidentiteter, men downloader ikke bytes og verificerer ikke downloadet
indhold. Dependency-graph resolution, automatisk release-selection og Maven-resolution er fremtidigt arbejde.
## 7. Betydningen af Semantisk Versionering
Format 1 accepterer kun stabile eksakte releases med alle tre numeriske komponenter. Det forudsætter ikke, at en
resolver altid vælger den numerisk højeste version: hard constraints afgrænser de gyldige valg, og policy/dependency
hints kan på sigt foretrække en lavere kompatibel release.
## 8. Componenter og det centrale Registry
En Nenjim component er en færdigkonstrueret byggeklods, som er klar til brug. Rollen som service, applikation,
algoritme eller adapter og klassifikationen som aktiv proces er uafhængige dimensioner. En service eksponerer et
domain API, mens en applikation beskriver en user-facing eller orchestration-rolle. Registry-API'et bruger det
fælles ord `component` og ikke `plugin`, og `NenjimComponent` er en ren marker interface. En component, som Hub'en
kan starte og tracke, implementerer desuden den canonical `com.r35157.nenjim.process.NenjimProcess`, som kun lover
`start()` og ikke en fælles stop-, restart- eller health-operation. En service- eller applikationsimplementation kan
derfor også være en process; passive componenter er det ikke.
Ticker følger denne opdeling: almindelige consumers modtager `TickerService` med kun latest-price query, mens det
samme `TickerServiceImpl`-objekt separat er en `NenjimProcess`, som Hub'en starter. Tickerens source-lifecycle er
implementation-owned; `PriceSource`-instanser startes internt, og en fejlet Ticker-start stopper de sources, som nåede
at starte. Der findes ingen almindelig Ticker-serviceoperation til start eller stop.
Evelyn IOU Burner følger samme opdeling, selv om dens nuværende domain-service-view er tomt:
`EvelynIOUBurnerService` giver typed Registry- og dependency-identitet uden eksterne håndtag, mens det samme
`EvelynIOUBurnerServiceImpl`-objekt separat er en Hub-styret `NenjimProcess`. Kun process-view'et eksponerer startup;
hverken service-interfacet eller implementationen eksponerer almindelig stop eller restart.
En `NenjimComponentId` er registration metadata i ét Registry og er ikke en egenskab ved objektet. ID-formatet er
lower-case ASCII med dot-separated segments og valgfrie interne bindestreger, f.eks. `nenjim.registry.service` og
`assetaz.price-source.raydium-pool.eve-usdt`. Det samme objekt må registreres under flere forskellige ID'er, og én
registration bliver automatisk synlig gennem alle component interfaces, som objektet implementerer. Registry-
service- og process-view af Registry-objektet har derfor samme ID og samme object identity.
`NenjimRegistryService` er det read-only katalog, som almindelige consumers modtager gennem constructor injection.
Det kan returnere et immutable registration-order snapshot af ID'er for et component interface eller slå ét ID op
gennem et forventet interface. Et manglende ID giver `null`; et eksisterende ID med forkert interface er en caller
programming error. Public API'et kan ikke registrere eller fjerne componenter.
`NenjimHubImpl` repræsenterer én kørende Nenjim runtime. Hub'en læser deployment-konfigurationen, ejer dependency
wiring og lifecycle, konstruerer hele den nuværende passive component graph og konstruerer kun de processer, som er
valgt til autorun. Et implementation-only Registry assembly udleverer read-only service-view, process-view af det
samme Registry-objekt og en separat `NenjimRegistryServiceManager`. Manageren er kun Hub'ens passive
registrationshandle: den er hverken component eller process, har ingen `start()` eller service-accessor, registreres
ikke og starter aldrig en component.
Et tilsvarende implementation-only Journal assembly udleverer read-only service-view, process-view af det samme
Journal-objekt og en separat passiv `JournalServiceManager`. Journal-manageren registreres ikke og giver kun adgang
til root og refresh gennem et package-private admin-view; almindelige `JournalService`-consumers kan ikke udlede
administrationsadgangen.
Alle moderne JavaFX-applikationer deler ét JVM-wide JavaFX toolkit. Det repræsenteres af
`JavaFxRuntimeService`, mens samme `JavaFxRuntimeServiceImpl`-objekt også implementerer `NenjimProcess` for Hub'ens
lifecycle. Når den er valgt, konstruerer og registrerer Hub'en objektet som `nenjim.javafx-runtime.service`; den skal
stå før valgte JavaFX-applikationer i autorun-listen, så dens service-view kan injiceres efter vellykket startup. Kun
processens `start()` må kalde `Platform.startup(...)`; `runLater(...)` starter aldrig toolkit'et implicit og afviser
arbejde, indtil startup er lykkedes. Startup venter på JavaFX Application Thread og sætter
`Platform.setImplicitExit(false)`. Udelades alle JavaFX-processer, konstruerer Hub'en ikke runtime-objektet og
initialiserer ikke toolkittet.
Den package-private `NenjimRegistryServiceAdmin` er managerens interne mutationsadgang. Den må ikke gøres public,
fordi almindelige consumers ellers kunne caste deres Registry-service til mutationsinterfacet og bryde det
read-only service contract. Registration starter ikke et objekt, og Registry'et forbliver internt åbent for senere
registration efter startup.
### 8.1 Obligatorisk `NenjimHub.conf` format 1
Hub'en læser kun `conf/NenjimHub.conf`. Filen skal eksistere som en læsbar regular file; en manglende eller ugyldig
fil stopper startup, før Registry startes. `conf/NenjimHub.conf.example` er kun en versioneret copy-template: Hub'en
læser, kopierer og genererer den aldrig og falder hverken tilbage til den eller til en hardcoded procesliste.
Den første faktiske entry skal være `FORMAT_VERSION=1`; før den må der kun stå blanke linjer og full-line
`#`-kommentarer. Den næste faktiske entry skal være den ene eksakte `[AUTO_START]`-header. Hver aktiv linje i
sektionen er ét `NenjimComponentId`, og rækkefølgen bevares præcist. Leading/trailing whitespace ignoreres, en trimmet
linje som begynder med `#` er en kommentar, og `#` efter faktisk indhold starter en trailing kommentar. Inde i
sektionen fortsætter en comment-only line listen, mens den første whitespace-only line afslutter sektionen; EOF kan
også afslutte den. Efter afslutningen tillades kun blanke linjer og kommentarer. Formatet har ingen quoting eller
escaping for `#`.
Hele listen valideres før første proces-start. Ugyldige, duplikerede, ukendte og passive component-ID'er afvises.
Registry skal stå præcis én gang først og Journal præcis én gang som nummer to. De kendte valgbare processer er:
```text
nenjim.registry.service
nenjim.journal.service
assetaz.ticker.default
jupiter-perps-alarm.default
evelyn.service.prod
evelyn.service.test
evelyn.iou-burner.prod
nenjim.javafx-runtime.service
evelyn.mission-control.default
nenjim.process-manager.default
```
Efter validering konstruerer og starter Hub'en Registry, registrerer det som `nenjim.registry.service` og registrerer
startup-milepælen. Derefter konstrueres og registreres hele den nuværende passive graph uden at starte workers eller
processer. Først herefter behandles resten af `[AUTO_START]` i den angivne rækkefølge, begyndende med Journal. Hver
valgt proces konstrueres først, når dens linje nås, registreres før `start()` og tilføjes kun til Hub'ens immutable
ordered snapshot, når `start()` returnerer. Journal er derfor stadig den anden succesfulde proces, selv om den passive
graph konstrueres mellem Registry- og Journal-start.
`[AUTO_START]` er foreløbig deploymentets komplette, manuelt ordnede proces-dependency closure. Hub'en finder,
indsætter eller omordner ikke dependencies. En krævet proces skal stå før sin consumer; eksempelvis skal
`nenjim.javafx-runtime.service` stå før Mission Control og Process Manager. Mangler forgængeren, stopper konstruktion
eller startup fail-fast uden implicit reparation. En udeladt proces bliver hverken konstrueret, registreret, startet
eller vist af Process Manager. Den versionerede template vælger kun de obligatoriske Registry- og Journal-processer;
alle øvrige processer skal tilføjes eksplicit til deploymentets private konfiguration.
Et minimalt headless eksempel, svarende til den versionerede template, er:
```text
FORMAT_VERSION=1
[AUTO_START]
nenjim.registry.service
nenjim.journal.service
```
Tickerens egen component-konfiguration vælger fortsat eksplicit
`assetaz.price-source.raydium-pool.eve-usdt` gennem Registry'et; den hardcodede price source er registreret, men
inaktiv. Fejl under passiv konstruktion, valgt proceskonstruktion, registration eller startup stopper straks og
forhindrer alle senere processer. Tidligere succeser forbliver synlige, den fejlende proces registreres ikke som
startet, og der udføres ingen generic rollback. Online point og det afsluttende blocking wait nås kun efter alle
konfigurerede processer er startet. Process Manager viser fortsat Hub'ens faktiske succesfulde snapshots og ikke den
ønskede konfigurationsliste.
`com.r35157.nenjim.hubd.Main` er kun det yderste Java-bootstrap. `Main.main(...)` konstruerer `NenjimHubImpl` gennem
det globale `NenjimHub`-interface og kalder `start()` præcis én gang. Hub'en ejer også det eksisterende online point
og den afsluttende blocking wait. Hub'en er hverken component eller process og registreres ikke i sit eget Registry.
Det implementerede Registry er ét katalog og er ikke et authorization-system. Contexts, artifact-version semantics,
resolution, classloading, automatic discovery, public runtime loading/registration, removal, persistence, events,
automatic dependency construction/start-all, usage tracking og permissions er fremtidigt arbejde. Den nuværende
autorun-fil er ikke Context resolution eller process discovery. Journal publishing, remote synchronization og
signature validation er ligeledes ikke implementeret.
## 9. Integration med AssetAZ og økonomisk aktivitet
Økonomi, publicering og distribution er fremtidig vision. Format 1 beskriver alene artifaktviden; det indeholder
ingen betalings-, salgs- eller transaktionsadfærd.
## 10. Installation af en NenjimHub
For at NenjimHub'en kan køre godt, skal den leve i et samspil med nogle andre komponenter. Dette kan sættes op på
forskellige måder. Efterfølgende er et eksempel på en opsætning i en Virtuel Maskine, eller en lille fysisk host.
Bemærk at ECC ram er kritisk for stabiliteten, specielt hvis NenjimHub'en skal arbejde med finansielle aktiviteter.
Efterfølgende er en beskrivelse af hvordan man laver en opsætning som virker. Du kan lave specielle rettelser, hvis
du forstår hvad du laver. Det vil dog være en god idé at følge en standard opsætning, så alt bliver lidt nemmere
at styre.
## 10.1 Reference opsætning af Fysisk eller Virtuel Maskine (VM) til NenjimHub
Her beskrives hvordan man laver en standard opsætning af en NenjimHub. Denne opsætning vil også blive refereret til
som reference opsætningen, sådan at man har et fælles udgangspunkt at tale ud fra.
## 10.1.2 Minimale system krav
Det er klart at jo mere NenjimHub'en skal lave, jo større er hardware kravene, men med en 'NenjimHub Reference
Configuration', får du en opsætning der kan starte op og som du kan bygge videre fra.
Derfor er kravene til opsætningen meget moderate.
* CPU: 64bit x86
* RAM: 2GB (Helst ECC)
* Storage: 2x16GB (for RAID)
* Networking: Public IPv4 access
## 10.1.3 Basis Operativ System installation
Hvis du installerer som en VM (Proxmox), så brug følgende:
* Aktiver Qemu Agent
* Virtual Machine type: q35
* BIOS: OVMF (UEFI - med EFI disk + Pre-Enrolled keys)
* Storage: 2 x 16GB (SSD emulation + Discard)
* CPU: 2 CPU cores (type: x86-64-v2-AES)
* Memory: 2GB (Minimum 1GB + Ballooning)
* Network: Adgang til et netværk med DHCP service (Fravælg Proxmox firewall)
Installer basis Operativ System:
- Boot installations medie for Debian-13
- Vælg defaults med mindre andet angivet nedenfor
- User setup:
- Full name: System Operator
- Username: sysop
- Partition disks:
- Manual
- Lav nye tomme partitions tabeller
- Disk 1:
- Partition 1
- Size: 512MB
- Name: EFI1
- Use as: EFI System Partition
- Bootable: on
- Partition 2
- Size: 8GB
- Name: ROOT1
- Use as: Ext4
- Label: ROOT
- Partition 3
- Size: 8.7GB (Resten af disken)
- Name: ZFS1
- Use as: do not use
- Disk 2:
- Partition 1
- Size: 512MB
- Name: EFI2
- Use as: do not use
- Partition 2
- Size: 8GB
- Name: ROOT2
- Use as: do not use
- Partition 3
- Size: 8.7GB (Resten af disken)
- Name: ZFS2
- Use as: do not use
- Acceptér ikke at opsætte swap endnu.
- Software selection:
- SSH server
- standard system utilities
- Reboot after installation
Install additional tools:
```
$ su -
# apt-get update; apt-get install -y htop zram-tools sudo tmux
# usermod -aG sudo sysop
(Log ud og ind igen)
# sudo bash
# vi /etc/default/zramswap
(Ret PRIORITY=1)
# sed -ri'.bak' '/^deb/ { /contrib/! s/$/ contrib/ }' /etc/apt/sources.list
# apt-get update
# mokutil --sb-state || sudo apt -y install mokutil && mokutil --sb-state
# apt install -y dkms sbsigntool shim-signed mokutil
# mokutil --import /var/lib/dkms/mok.pub
(Indtast en midlertidig adgangskode)
# apt-get install -y linux-headers-amd64 zfsutils-linux zfs-dkms zfs-zed
# reboot
(Når den blå skærm kommer, vælg 'Enroll MOK' og brug nu det password du valgte ovenover)
```
Opret ZFS pool:
```
$ sudo bash
# zpool create zfspool -O xattr=sa -o ashift=12 -O compression=lz4 -O checksum=sha512 -O dedup=on -O atime=off mirror /dev/sda3 /dev/sdb3
```
Opret swap:
```
$ sudo bash
# zfs create -V 4G -o compression=zle \
-o logbias=throughput -o sync=always \
-o primarycache=metadata -o secondarycache=none \
-o com.sun:auto-snapshot=false zfspool/swap
# mkswap -f /dev/zvol/zfspool/swap
# echo /dev/zvol/zfspool/swap none swap sw,pri=0 0 0 >> /etc/fstab
# swapon -av
```
Installér docker (using official repository instead of Debian repository):
```
$ sudo bash
# apt install -y apt-transport-https ca-certificates curl gpg
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian trixie stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# apt update
# apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# usermod -aG docker sysop
(Log out and in again)
```
Flyt docker storage til ZFS:
```
$ sudo bash
# systemctl stop docker
# zfs create -V 3G zfspool/var_lib_docker
# mkfs.ext4 /dev/zvol/zfspool/var_lib_docker
# rm -rf /var/lib/docker
# mkdir /var/lib/docker
# echo "/dev/zvol/zfspool/var_lib_docker /var/lib/docker ext4 defaults 0 2" >> /etc/fstab
# systemctl daemon-reload
# mount /var/lib/docker
# chmod 710 /var/lib/docker
# systemctl enable docker ; systemctl start docker
```
Installer ZeroTier:
```
$ sudo bash
# curl -fsSL 'https://raw.githubusercontent.com/zerotier/ZeroTierOne/main/doc/contact%40zerotier.com.gpg' | gpg --import
(Importer ZeroTier's signeringsnøgle)
# gpg --fingerprint contact@zerotier.com
(Check nøglen)
#if z=$(curl -fsSL 'https://install.zerotier.com/' | gpg --decrypt); then
echo "$z" | sudo bash
else
echo "Signaturtjek mislykkedes – kører ikke scriptet." >&2
exit 1
fi
(Henter og dekryperer (verificerer) install-scriptet og kører det kun hvis signaturen er OK)
```
Konfigurer ZeroTier net:
- Login på 'https://my.zerotier.com'
- Opret et nye netværk 'Create A Network'
(Som skal forbinde terminaler og AssetAZHub)
- Klik på det nye netværk
- Basic/Name: 'assetaz-&lt;ditid&gt;'
(f.eks 'assetaz-mortengh')
- Vælg et IP range, f.eks 192.168.192.*
- Forbind AssetAZHub til netværket (netværks id'et kan ses på websiden):
- # zerotier-cli join a8bea75acfa45477
- Autoriser den nye maskine på nettet. Dette gøres ved at trykke på 'Edit' ved maskinen på websiden, som nu
burde kunne se den. Autoriser den, giv den et navn og evt. en beskrivelse.
- Tryk 'Add IP' og giv den en IP adresse inden for det range du har valgt oven over (f.eks 192.168.192.1). Tryk 'Save'
- Se at AssetAZHub' nu kan pinge sig selv:
# ping 192.168.192.1
Forbind terminal (f.eks en Android telefon)
- Installer ZeroTier-One
- Forbind til det samme net som hub'en.
- Autoriser terminalen inde på ZeroTiers web side og set terminalen ip adresse til noget kendt f.eks 192.168.192.2.
(Så kører hub'en på *.1 og terminalen på *.2)
Opsæt locale
Opret Docker baby swarm cluster
```
```
+28
View File
@@ -0,0 +1,28 @@
#<center><H1>Nenjim TestTool<br>Documentation</H1></center>
## Byg af NenjimTestTool
NOTE: NenjimTestTool ligger i øjeblikket i Cauldron, men skal flyttes ud i sit egent projekt senere. Indtil det sker
bygges NenjimTestTool sammen med alt det andet som ligger i Cauldron.
``` bash
~/projects/r35157_cauldron$ ./mvnw package
```
## 12. Opstart af NenjimTestTool
For at kunne starte NenjimTestTool, skal du have startet en NenjimHub instans, da NenjimTestTool startes igennem
Nenjim.
Der er jo forskellige interfaces til NenjimHub'en, men en nem måde er hvis du har aktiveret CLI interfacet til
NenjimHub'en - så kan du state den ved at følge følgende trace:
``` bash
$ telnet localhost 38010
Trying ::1...
Connected to localhost.
Escape character is '^]'.
NenjimHub 0.1 r35157_reference_implementation 0.0.0.1
OK
START crypto.r35157.nenjim.NenjimTestTool
OK (0ms)
```
Der åbnes nu et vindue for TestToolet.
+1
View File
@@ -0,0 +1 @@
#<center><H1>Soda Task Manager<br>Documentation</H1></center>
+166
View File
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SuWiMo Documentation</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
}
.container {
width: 80%;
max-width: 1000px;
margin: auto;
padding: 20px;
}
h1, h2, h3 {
color: #0056b3;
}
.section {
margin-bottom: 40px;
}
.highlight {
background-color: #e7f1ff;
padding: 10px;
border-left: 4px solid #0056b3;
margin-top: 20px;
}
.footer {
text-align: center;
padding: 20px;
border-top: 1px solid #ddd;
margin-top: 40px;
color: #777;
}
.token-logo {
width: 100px;
height: 100px;
}
pre, code {
background: #f4f4f4;
border-radius: 4px;
padding: 2px 6px;
}
pre {
padding: 10px;
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ccc;
padding: 6px;
text-align: left;
}
ul, ol {
margin-left: 1.2em;
}
</style>
</head>
<body>
<div class="container">
<h1>SuWiMo Documentation</h1>
<p>#<center><H1>SuWiMo<br>Documentation</H1></center></p><p><strong>Indholdsfortegnelse:</strong>
<!-- TOC -->
* [1. Introduktion og Formål](#1-introduktion-og-formål)
* [2. Arkitektur og Overordnet Design](#2-arkitektur-og-overordnet-design)
* [2.1 Central Hub (SuWiMoHub)](#21-central-hub-suwimohub)
* [2.2 Plugin-baseret Design](#22-plugin-baseret-design)
* [3. Sensorer](#3-sensorer)
* [3.1 Sensorinterface](#31-sensorinterface)
* [3.2 Statusniveauer](#32-statusniveauer)
* [3.3 Passiv Sensorlogik](#33-passiv-sensorlogik)
* [4. Hierarkisk Struktur (Træstruktur)](#4-hierarkisk-struktur-træstruktur)
* [4.1 Dynamisk Træ via Konfiguration](#41-dynamisk-træ-via-konfiguration)
* [4.2 Statuspropagering](#42-statuspropagering)
* [5. Notifikationssystem og Logging](#5-notifikationssystem-og-logging)
* [5.1 Notifier Interface](#51-notifier-interface)
* [5.2 Historik og Logging](#52-historik-og-logging)
* [6. Distribution og Decentralisering](#6-distribution-og-decentralisering)
* [6.1 AssetAZ Observables](#61-assetaz-observables)
* [6.2 Distribuerede Hubs](#62-distribuerede-hubs)
* [7. Brugsscenarier og Eksempler](#7-brugsscenarier-og-eksempler)
* [8. Fremtidige Funktioner / TODO](#8-fremtidige-funktioner--todo)
<!-- TOC --></p><p><h2>1. Introduktion og Formål</h2>
SuWiMo (Surveillance Without Moving) er et system udviklet til enkel og effektiv statusovervågning af mange uafhængige
sensorer organiseret i en hierarkisk træstruktur. Formålet med SuWiMo er at give et hurtigt, visuelt overblik over
systemets tilstand, hvor status let kan aflæses som grøn (OK), gul (advarsel) eller rød (kritisk fejl).</p><p>SuWiMo fokuserer på tillidsvalidering og identificering af afvigelser i realtid, snarere end detaljeret monitorering
eller datalogging som traditionelle overvågningssystemer.</p><p><h2>2. Arkitektur og Overordnet Design</h2>
<h3>2.1 Central Hub (SuWiMoHub)</h3>
SuWiMoHub fungerer som den centrale orchestrator, der står for al polling, statusvurdering og statuspropagering i
træstrukturen. Hubben håndterer ikke selv sensorlogik, men trækker på plugins via interfaces.
<ul><ul><li>Centraliseret styring af polling-interval.</li>
<li>Indlæsning af sensor- og notifier-plugins.</li>
<li>Dynamisk opbygning og opdatering af status-træet.</li></p><p><h3>2.2 Plugin-baseret Design</h3>
SuWiMo benytter en modulær plugin-arkitektur, hvor forskellige typer af plugins implementerer specifikke interfaces
efter deres funktion:
<li><strong>Sensor-plugins</strong> implementerer sensorinterface til overvågning og statusrapportering.</li>
<li><strong>Notifier-plugins</strong> implementerer notifierinterface til at sende notifikationer ved statusændringer.</li>
<li><strong>Logger-plugins</strong> implementerer logningsinterface til historisk registrering af statusændringer.</li></p><p>Disse plugins registreres og administreres af Nenjim-systemet, hvilket sikrer nem distribution, versionering og
udskiftelighed.</p><p><h2>3. Sensorer</h2>
<h3>3.1 Sensorinterface</h3>
Sensorer implementerer et simpelt, standardiseret interface:</p><pre><code class="java">public interface SuwimoSensor {
SensorStatus poll();
}</p><p>public class SensorStatus {
StatusLevel level; // GREEN, YELLOW, RED
String message; // Kort forklaring på tilstand
}
</code></pre><p><h3>3.2 Statusniveauer</h3>
<li><strong>Green</strong>: Normal drift.</li>
<li><strong>Yellow</strong>: Advarsel, kræver opmærksomhed.</li>
<li><strong>Red</strong>: Kritisk fejl, kræver øjeblikkelig handling.</li></p><p><h3>3.3 Passiv Sensorlogik</h3>
Sensorer kører ikke aktivt. De er passive komponenter, som kun evaluerer og returnerer deres status, når hubben
poller dem.</p><p><h2>4. Hierarkisk Struktur (Træstruktur)</h2>
<h3>4.1 Dynamisk Træ via Konfiguration</h3>
Træstrukturen defineres i en JSON-konfigurationsfil, hvor hver node har et unikt ID, et navn, en valgfri sensor og en
parent-reference. Eksempel:</p><pre><code class="json">[
{ "id": "internet", "name": "Internetforbindelse", "sensor": "PingGoogle", "parent": null },
{ "id": "zfs-check", "name": "ZFS array", "sensor": "ZfsStatus", "parent": "server01" },
{ "id": "server01", "name": "Serverrummet", "sensor": null, "parent": null }
]
</code></pre><p><h3>4.2 Statuspropagering</h3>
Den dårligste status blandt børn og egen sensor propagerer op gennem træet. Det gør det muligt at se den værste
tilstand hurtigt fra en node eller træets rod.</p><p><h2>5. Notifikationssystem og Logging</h2>
<h3>5.1 Notifier Interface</h3>
Notifikationskomponenter implementerer et interface, der reagerer på statusændringer:</p><pre><code class="java">public interface SuwimoNotifier {
void statusChanged(SensorEvent event);
}</p><p>public class SensorEvent {
String sensorId;
StatusLevel oldStatus;
StatusLevel newStatus;
String message;
ZonedDateTime timestamp;
}
</code></pre><p><h3>5.2 Historik og Logging</h3>
Historiske log-events registreres hver gang en sensor ændrer tilstand. Denne funktionalitet implementeres som et
separat plugin, der eksempelvis kan logge til fil, database eller anden ekstern tjeneste.</p><p><h2>6. Distribution og Decentralisering</h2>
<h3>6.1 AssetAZ Observables</h3>
SuWiMoHub eksponerer sin status som en AssetAZ Observable. Det gør det muligt for andre hubs eller AssetAZ-instanser
at abonnere på sensorstatus fra eksterne hubs.</p><p><h3>6.2 Distribuerede Hubs</h3>
Flere SuWiMoHubs kan installeres decentralt, tæt på de ressourcer de overvåger, og udveksle sensorstatus via AssetAZ's
P2P-netværk. Dette sikrer effektiv isolation af fejl og lokal robusthed.</p><p><h2>7. Brugsscenarier og Eksempler</h2>
Eksempler på anvendelser af SuWiMo:
<li>Overvågning af servermiljøer (CPU, RAM, ZFS arrays)</li>
<li>Netværksstatus (Internet-ping, DNS-opløsning)</li>
<li>Overvågning af applikationstilgængelighed og integritet (HTTP-checks med checksum)</li>
<li>Husovervågning (temperatur, dørsensorer, strøm)</li></p><p><h2>8. Fremtidige Funktioner / TODO</h2>
<li>Implementering af GUI med træstruktur (træk-og-slip sensororganisering)</li>
<li>Avanceret historikvisning (grafer og mønstergenkendelse)</li>
<li>Yderligere notifier-plugins (push-notifikationer, SMS, Discord, syslog)</li>
<li>Avancerede permissions og adgangskontrol</li>
<li>Automatisk timeout-tilstand (sensor bliver rød efter manglende respons)</li>
</ul></ul></p>
<div class="footer">
<p>Part of the r35157 project. See also AssetAZ, Nenjim, Evelyn.</p>
</div>
</div>
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
#<center><H1>SuWiMo<br>Documentation</H1></center>
**Indholdsfortegnelse:**
<!-- TOC -->
* [1. Introduktion og Formål](#1-introduktion-og-formål)
* [2. Arkitektur og Overordnet Design](#2-arkitektur-og-overordnet-design)
* [2.1 Central Hub (SuWiMoHub)](#21-central-hub-suwimohub)
* [2.2 Plugin-baseret Design](#22-plugin-baseret-design)
* [3. Sensorer](#3-sensorer)
* [3.1 Sensorinterface](#31-sensorinterface)
* [3.2 Statusniveauer](#32-statusniveauer)
* [3.3 Passiv Sensorlogik](#33-passiv-sensorlogik)
* [4. Hierarkisk Struktur (Træstruktur)](#4-hierarkisk-struktur-træstruktur)
* [4.1 Dynamisk Træ via Konfiguration](#41-dynamisk-træ-via-konfiguration)
* [4.2 Statuspropagering](#42-statuspropagering)
* [5. Notifikationssystem og Logging](#5-notifikationssystem-og-logging)
* [5.1 Notifier Interface](#51-notifier-interface)
* [5.2 Historik og Logging](#52-historik-og-logging)
* [6. Distribution og Decentralisering](#6-distribution-og-decentralisering)
* [6.1 AssetAZ Observables](#61-assetaz-observables)
* [6.2 Distribuerede Hubs](#62-distribuerede-hubs)
* [7. Brugsscenarier og Eksempler](#7-brugsscenarier-og-eksempler)
* [8. Fremtidige Funktioner / TODO](#8-fremtidige-funktioner--todo)
<!-- TOC -->
## 1. Introduktion og Formål
SuWiMo (Surveillance Without Moving) er et system udviklet til enkel og effektiv statusovervågning af mange uafhængige
sensorer organiseret i en hierarkisk træstruktur. Formålet med SuWiMo er at give et hurtigt, visuelt overblik over
systemets tilstand, hvor status let kan aflæses som grøn (OK), gul (advarsel) eller rød (kritisk fejl).
SuWiMo fokuserer på tillidsvalidering og identificering af afvigelser i realtid, snarere end detaljeret monitorering
eller datalogging som traditionelle overvågningssystemer.
## 2. Arkitektur og Overordnet Design
### 2.1 Central Hub (SuWiMoHub)
SuWiMoHub fungerer som den centrale orchestrator, der står for al polling, statusvurdering og statuspropagering i
træstrukturen. Hubben håndterer ikke selv sensorlogik, men trækker på plugins via interfaces.
- Centraliseret styring af polling-interval.
- Indlæsning af sensor- og notifier-plugins.
- Dynamisk opbygning og opdatering af status-træet.
### 2.2 Plugin-baseret Design
SuWiMo benytter en modulær plugin-arkitektur, hvor forskellige typer af plugins implementerer specifikke interfaces
efter deres funktion:
- **Sensor-plugins** implementerer sensorinterface til overvågning og statusrapportering.
- **Notifier-plugins** implementerer notifierinterface til at sende notifikationer ved statusændringer.
- **Logger-plugins** implementerer logningsinterface til historisk registrering af statusændringer.
Disse plugins registreres og administreres af Nenjim-systemet, hvilket sikrer nem distribution, versionering og
udskiftelighed.
## 3. Sensorer
### 3.1 Sensorinterface
Sensorer implementerer et simpelt, standardiseret interface:
```java
public interface SuwimoSensor {
SensorStatus poll();
}
public class SensorStatus {
StatusLevel level; // GREEN, YELLOW, RED
String message; // Kort forklaring på tilstand
}
```
### 3.2 Statusniveauer
- **Green**: Normal drift.
- **Yellow**: Advarsel, kræver opmærksomhed.
- **Red**: Kritisk fejl, kræver øjeblikkelig handling.
### 3.3 Passiv Sensorlogik
Sensorer kører ikke aktivt. De er passive komponenter, som kun evaluerer og returnerer deres status, når hubben
poller dem.
## 4. Hierarkisk Struktur (Træstruktur)
### 4.1 Dynamisk Træ via Konfiguration
Træstrukturen defineres i en JSON-konfigurationsfil, hvor hver node har et unikt ID, et navn, en valgfri sensor og en
parent-reference. Eksempel:
```json
[
{ "id": "internet", "name": "Internetforbindelse", "sensor": "PingGoogle", "parent": null },
{ "id": "zfs-check", "name": "ZFS array", "sensor": "ZfsStatus", "parent": "server01" },
{ "id": "server01", "name": "Serverrummet", "sensor": null, "parent": null }
]
```
### 4.2 Statuspropagering
Den dårligste status blandt børn og egen sensor propagerer op gennem træet. Det gør det muligt at se den værste
tilstand hurtigt fra en node eller træets rod.
## 5. Notifikationssystem og Logging
### 5.1 Notifier Interface
Notifikationskomponenter implementerer et interface, der reagerer på statusændringer:
```java
public interface SuwimoNotifier {
void statusChanged(SensorEvent event);
}
public class SensorEvent {
String sensorId;
StatusLevel oldStatus;
StatusLevel newStatus;
String message;
ZonedDateTime timestamp;
}
```
### 5.2 Historik og Logging
Historiske log-events registreres hver gang en sensor ændrer tilstand. Denne funktionalitet implementeres som et
separat plugin, der eksempelvis kan logge til fil, database eller anden ekstern tjeneste.
## 6. Distribution og Decentralisering
### 6.1 AssetAZ Observables
SuWiMoHub eksponerer sin status som en AssetAZ Observable. Det gør det muligt for andre hubs eller AssetAZ-instanser
at abonnere på sensorstatus fra eksterne hubs.
### 6.2 Distribuerede Hubs
Flere SuWiMoHubs kan installeres decentralt, tæt på de ressourcer de overvåger, og udveksle sensorstatus via AssetAZ's
P2P-netværk. Dette sikrer effektiv isolation af fejl og lokal robusthed.
## 7. Brugsscenarier og Eksempler
Eksempler på anvendelser af SuWiMo:
- Overvågning af servermiljøer (CPU, RAM, ZFS arrays)
- Netværksstatus (Internet-ping, DNS-opløsning)
- Overvågning af applikationstilgængelighed og integritet (HTTP-checks med checksum)
- Husovervågning (temperatur, dørsensorer, strøm)
## 8. Fremtidige Funktioner / TODO
- Implementering af GUI med træstruktur (træk-og-slip sensororganisering)
- Avanceret historikvisning (grafer og mønstergenkendelse)
- Yderligere notifier-plugins (push-notifikationer, SMS, Discord, syslog)
- Avancerede permissions og adgangskontrol
- Automatisk timeout-tilstand (sensor bliver rød efter manglende respons)
+60
View File
@@ -0,0 +1,60 @@
# Tekniske termer
> **Dokumentstatus:** Journal-format version 1 og de tilhørende Java-typer, den interne parser, query-service,
> Hub-styret Journal-proces, JavaFX-runtime-service, passive service-managers, første version af det centrale
> component Registry og obligatorisk Hub-konfiguration format 1 er implementeret. Resolver-, Context-,
> classloader-, discovery-, runtime-loading-, retrieval- og publishing-termer beskriver kun fremtidig vision, når
> det er angivet.
| Term | Beskrivelse |
|------|-------------|
| Service | Det offentlige domæne-API, som applikationer og andre services bruger i daglig drift. Det eksponerer almindelige domæneoperationer, men ikke lifecycle, configuration, data-source administration, refresh, parser eller mutation af intern viden. En konkret serviceimplementation kan samtidig være en `NenjimProcess`, uden at lifecycle føjes til service-interfacet. |
| ServiceManager | Et separat, passivt administrations-API, som en composition/lifecycle owner beholder uden at give almindelige service-consumers administrative rettigheder. Journal-manageren eksponerer root og refresh, mens Registry-manageren eksponerer registration. En manager er ikke det daglige domain/query-API, er hverken component eller process og ejer ikke startup. |
| Nenjim component | En færdigkonstrueret Nenjim-byggeklods, som er klar til brug. `NenjimComponent` er en ren marker uden ID eller lifecycle state. Service, applikation, algoritme og adapter beskriver roller, mens process beskriver en uafhængig lifecycle-klassifikation. |
| Applikation | En user-facing eller orchestration-rolle. Rollen gør ikke i sig selv en component til en process; en aktiv applikationsimplementation kan separat implementere `NenjimProcess`. |
| NenjimProcess | Det canonical `com.r35157.nenjim.process.NenjimProcess` component interface for noget, som Hub'en kan starte og tracke. Det eksponerer kun `start()` og lover ikke stop, restart, health, Context eller dependency lookup. |
| Passiv component | En `NenjimComponent`, hvis implementation ikke deltager i Hub-lifecycle gennem det canonical `NenjimProcess`. Den nuværende Hub konstruerer og registrerer hele den passive dependency graph efter Registry-start uafhængigt af `[AUTO_START]`, men starter den ikke. |
| Valgt proces | Et kendt `NenjimProcess`-objekt, som Hub'en kun konstruerer, registrerer og starter, når dets ID forekommer i `[AUTO_START]`. Rollen som service eller applikation afgør ikke denne klassifikation. |
| NenjimHub | Den globale ejer af én kørende Nenjim runtime. Hub'en validerer `conf/NenjimHub.conf` før proces-start, bootstrapper Registry, konstruerer den passive graph og konstruerer/registrerer/starter kun valgte processer i eksakt konfigurationsrækkefølge. Den tracker succesfulde start-ID'er, erklærer online point og ejer den afsluttende runtime wait, men er hverken component eller process og registreres ikke i sit eget Registry. |
| AUTO_START | Den obligatoriske, ordered sektion i `NenjimHub.conf` format 1. Den er deploymentets komplette manuelle proces-dependency closure; Registry og Journal skal være første og anden entry. Den udtrykker ikke Context resolution, discovery eller automatisk dependency construction. |
| NenjimComponentId | Canonical lower-case Registry lookup identity med dot-separated segments og valgfrie interne bindestreger. ID'et er registration metadata i ét Registry, ikke object state, authorization eller artifact-version identity. |
| NenjimRegistryService | Read-only component-katalog for almindelige consumers. Det giver immutable registration-order ID snapshots og typed lookup, men ingen registration, removal, loading, lifecycle, permission eller persistence administration. |
| NenjimRegistryServiceAdmin | Package-private mutationsinterface i referenceimplementationen. Det er ikke en component, er ikke indexeret og må ikke gøres public, fordi almindelige consumers ellers kunne caste Registry-serviceobjektet til mutationsinterfacet. |
| NenjimRegistryServiceManager | Public, passivt administrationshandle med kun `registerComponent(...)`. Det holdes privat af Hub'en, delegerer til det interne admin-view, er hverken component eller process, registreres ikke og ejer ingen lifecycle eller composition. |
| Component registration | Bindingen mellem ét `NenjimComponentId` og ét object. Én registration indexeres under alle objektets component interfaces; flere forskellige ID'er må pege på præcis samme object instance. |
| Journal | Et immutable, komplet snapshot af al aktuel viden om præcis én artifakt og dens kendte releases. Journalen er aldrig en delta. |
| ArtifactCoordinate | Journal-kædens stabile identitet, udledt som `<GROUP>-<MODULE>-<ARTIFACT>`, f.eks. `com_r35157_nenjim-hubd-api`. Der findes intet separat `JournalId`. |
| JournalVersion | Journal-snapshottets publiceringstid som UTC med millisekundpræcision: `uuuuMMddHHmmssSSS'Z'`. Den er forskellig fra en releases `PUBLISHED_AT`. |
| Journal-kæde | Én genesis og en lineær række immutable snapshots for samme coordinate. Hver efterfølger refererer med SHA-256 til forgængerens eksakte UTF-8-tekst. |
| Historisk worldview | Hele det snapshot, som var nyest på eller før en valgt `JournalVersion`; det beregnes ikke ved at filtrere et nyere snapshot efter release-tid. |
| Artifact release | En eksakt stabil `major.minor.patch`-version af én artifakt med per-release licens, publication time, content digests, content size og dependencies. |
| DESCRIPTION | Obligatorisk single-quoted artifactbeskrivelse i `[METADATA]`; decoded tekst kan ændres i en senere Journal-revision. |
| INFORMATION_URI | Valgfri single-quoted, syntaktisk gyldig absolut URI. Alle schemes er tilladt; indholdet åbnes eller fortolkes ikke automatisk. |
| LICENSE | Obligatorisk single-quoted fri tekst for hver release. Den normaliseres eller juridisk fortolkes ikke; fremtidig filtrering er ikke implementeret. |
| ContentDigest | Valideret indholdsidentitet. Format 1 understøtter `sha256` og CIDv1 (`cid1`), men downloader eller verificerer ikke artifact-bytes. |
| SemanticVersion | Én eksakt stabil artifaktrelease med major, minor og patch. Prerelease/build metadata understøttes ikke i format 1. |
| VersionExpression | Et normaliseret sæt acceptable releases: exact, minor-serie eller inclusive/exclusive/open range. Mathematical comparison syntax understøttes ikke. |
| DependencyTarget | Et explicit scheme og et scheme-specifikt target. Format 1 implementerer kun `nenjim:<artifact-coordinate>`; Maven-resolution er fremtidig. |
| DEPENDS_ON | Den hårde basisramme af acceptable releases for én dependency-relation. |
| EXCLUDES | En hård relationel regel, som fjerner versioner fra én bestemt `DEPENDS_ON`. Individuelle beskrivelser bevares. |
| PREFERRED | Et hint til en delmængde af `DEPENDS_ON - EXCLUDES`; det kan aldrig gøre en ellers ugyldig version gyldig. |
| BLACKLIST | En hård artifact-policy: matchende releases må ikke vælges. |
| DISCOURAGED | Et negativt artifact-policy-hint: matchende releases bør undgås, men er fortsat selectable. |
| RECOMMENDED | Et positivt hint til én eksakt release, der findes i samme snapshot. Højeste version er kun en fremtidig fallback, ikke en ubetinget selection-regel. |
| JournalService | Registrerbart, filesystem-uafhængigt og read-only Service-API for current og historical Journal-queries. Det eksponerer ingen lifecycle-, path-, refresh-, parser- eller mutationsoperationer, og queries er gated indtil vellykket proces-startup. |
| JournalServiceManager | Passivt, uregistreret administrationshandle for én Journal-service med kun normalized data root og explicit refresh. Hub'en beholder det; det har ingen start, stop, service-accessor eller query-operationer. |
| Journal referenceimplementation | Ét package-private `JournalServiceImpl`-objekt er både `JournalService` og `NenjimProcess`; en separat `JournalServiceManagerImpl`, intern admin-view, parser, ingestion, chain state og filesystem-loader ligger under `com.r35157.nenjim.service.journal.impl.ref` uden at lække gennem public signatures. |
| JavaFX runtime | Det ene JVM-wide JavaFX toolkit, som udfører UI-arbejde på JavaFX Application Thread. Når processen er valgt, ejer Hub'en dets eneste startup-sti gennem `JavaFxRuntimeServiceImpl` og holder det aktivt efter sidste lukkede vindue med `Platform.setImplicitExit(false)`. En headless konfiguration kan udelade runtime og alle JavaFX-applikationer helt. |
| JavaFxRuntimeService | Registrerbart service-view med kun non-null `runLater(Runnable)`. Samme referenceimplementation er en Hub-styret `NenjimProcess`; service-consumers får den gennem constructor injection, men får ingen lifecycle-, status-, configuration-, window-, event-, stop- eller restart-operationer. |
| TickerService | Lifecycle-frit domain/query-view med kun latest-price lookup. Samme `TickerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; det ejer `PriceSource`-startup internt og stopper allerede startede sources som rollback, hvis Ticker-start fejler, men eksponerer ingen almindelig start- eller stop-operation. |
| EvelynIOUBurnerService | Tomt, lifecycle-frit domain-service-view, som bevarer typed Registry- og dependency-identitet, indtil eksterne domain-operationer findes. Samme `EvelynIOUBurnerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; service-consumers får ingen start-, stop- eller restart-operationer. |
| Context | Fremtidig beskrivelse af ønskede/resolverede artifactvalg og isoleret runtime; ikke implementeret af Journal-laget eller første Registry-version. |
| Resolver | Fremtidig komponent for dependency graph og release-selection; Journal-laget repræsenterer reglerne, men vælger ikke en graf. |
# Domænespecifikke termer
| Term | Beskrivelse |
|------|-------------|
| Position Value | Den samlede nutidsværdi af en position (nuværende pris × antal). |
| Base Value | Den pris det har kostet at købe positionen (gennemsnitlig købspris × antal). |
| Margin Value | Den pris man faktisk har betalt for en gearet position (base value / gearing). |
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-05
@@ -0,0 +1,28 @@
## Context
Before this change, `Evelyn` declared `@NotNull String getStatusReport()`, `EvelynImpl` returned `"Hello World!"`, and no other local source called it. Mission Control presents parallel Production and Test tab sets. The API and reference implementations must remain separated by package.
## Goals / Non-Goals
**Goals:** Define a typed immutable measurement contract, replace the text API, and render each environment's Evelyn history in its corresponding existing Overview tab without disturbing the Production/Test tabs or styling.
**Non-Goals:** Real index calculation, persistence, refresh/polling after view creation, environment discovery, dependency resolution, final Nenjim environment wiring, and automated tests.
## Decisions
- Add `EvelynStatusIndexPoint` beside the Evelyn public API as an immutable value type. Use `Instant` for the timestamp and `BigDecimal` for each named index so time semantics are explicit and decimal values retain precision. A map or six-element list was rejected because it would lose compile-time names and completeness.
- Replace, rather than supplement, `getStatusReport()` with `@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory()`. The implementation returns an immutable hardcoded list in chronological order; callers must also tolerate an empty list. Keeping the text API was rejected because the issue intentionally replaces that contract.
- Make `EvelynMissionControlImpl` accept separate Production and Test Evelyn references. Fetch each reference's history while constructing its corresponding Overview so the two tabs remain independent even when the reference implementations currently return identical hardcoded values.
- Construct a distinct chart, axes, series, and data nodes for each Overview. Sharing a JavaFX `Node` is not valid because a node cannot have two parents.
- Have `NenjimHubImpl` create two separate `EvelynImpl` instances as temporary Production and Test placeholders and pass both to Mission Control. Future Nenjim wiring will supply the actual environment-specific Evelyn references; environment discovery and dependency resolution are outside issue #59.
- Use a time-capable numeric X-axis derived from each point's timestamp and a numeric Y-axis. Compute the largest absolute index magnitude across all series and use its negative and positive values as equal bounds; use a small symmetric fallback span when history is empty or all values are zero so zero remains visible. JavaFX chart construction remains in the reference implementation.
## Risks / Trade-offs
- [Hardcoded history can look authoritative] → Keep values clearly sample-only and isolate them in `EvelynImpl` for later replacement.
- [One extreme index compresses the other lines] → Accept a shared scale because direct comparison and a common zero baseline are required.
- [Breaking API removal affects downstream consumers outside this repository] → Publish the typed replacement in the same release and call out the method migration.
## Migration Plan
Add the value type, replace the interface method and reference implementation, construct distinct Production and Test Overview charts from their corresponding Evelyn references, and compile the project. Roll back the API and Overview changes together if needed; no stored data requires migration.
@@ -0,0 +1,27 @@
## Why
Evelyn exposes only an unstructured textual status report, which prevents Mission Control from plotting status changes over time. A typed historical API will make the six status indexes directly consumable by the existing Overview views.
## What Changes
- **BREAKING** Replace `Evelyn.getStatusReport()` with `List<EvelynStatusIndexPoint> getStatusIndexHistory()`.
- Add a typed measurement point containing a timestamp and the six Evelyn status indexes.
- Initially provide oldest-to-newest hardcoded sample history from the reference implementation.
- Give Mission Control separate Production and Test `Evelyn` references and plot each reference's history in its corresponding Overview tab using separate chart instances with dynamic Y-axes symmetric around a visible zero.
## Capabilities
### New Capabilities
- `evelyn-status-index-history`: Typed historical status-index access and Mission Control visualization.
### Modified Capabilities
None.
## Impact
- Affects the Evelyn public API and reference implementation in `com.fanitas.evelyn.core`.
- Affects Evelyn Mission Control's existing Overview content and introduces explicit Production/Test Evelyn wiring.
- Removes the textual status-report API; the repository currently has no callers beyond its declaration and implementation.
- Adds no persistence, real index calculation, environment discovery, dependency resolution, external dependency, or automated tests.
@@ -0,0 +1,23 @@
## Purpose
Provide typed historical Evelyn status measurements and make all six indexes visible over time in Mission Control.
## ADDED Requirements
### Requirement: Evelyn provides historical status-index measurements
Evelyn SHALL expose a non-null list of typed measurement points ordered from oldest to newest. Each point SHALL contain a timestamp, Evelyn Price Index, EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index; the list MAY be empty.
#### Scenario: Status-index history is requested
- **WHEN** a caller requests Evelyn's status-index history
- **THEN** Evelyn returns a non-null, oldest-to-newest list whose points contain a timestamp and all six index values
### Requirement: Evelyn Mission Control visualizes status-index measurements
Evelyn Mission Control SHALL display historical measurements from separate Production and Test Evelyn references in their corresponding Overview tabs as six-line graphs, with timestamp on the X-axis and index value on the Y-axis. Each environment SHALL retrieve its own history and SHALL have a separate graph instance with a dynamic Y-axis range that is symmetric around zero and always displays zero as the desired state. The two histories MAY contain identical values but SHALL remain separate data sources.
#### Scenario: Production and Test Overviews are created with history
- **WHEN** Mission Control creates the Production and Test Overview views
- **THEN** it retrieves history from the corresponding Production and Test Evelyn references and creates a separate six-line graph for each environment whose symmetric Y-axis contains zero
#### Scenario: An environment has empty history
- **WHEN** Mission Control creates an environment's Overview view and that environment's Evelyn history is empty
- **THEN** it displays an empty six-series graph for that environment whose Y-axis still contains zero without substituting the other environment's history
@@ -0,0 +1,15 @@
## 1. Typed Status History
- [x] 1.1 Add the immutable `EvelynStatusIndexPoint` API value type with an `Instant` timestamp and six named `BigDecimal` index values.
- [x] 1.2 Replace `Evelyn.getStatusReport()` with the non-null `getStatusIndexHistory()` list contract.
- [x] 1.3 Implement an immutable, oldest-to-newest hardcoded sample history in `EvelynImpl`.
## 2. Mission Control Overview
- [x] 2.1 Update Mission Control to accept separate Production and Test Evelyn references and fetch each reference's history for its corresponding Overview tab.
- [x] 2.2 Update NenjimHub to supply two separate `EvelynImpl` placeholder instances and keep distinct six-series chart instances for the two histories.
- [x] 2.3 Configure a dynamic symmetric Y-axis around zero, including a usable zero-containing fallback range for empty or all-zero history.
## 3. Verification
- [x] 3.1 Compile the project and manually confirm each Overview tab renders its corresponding Evelyn history in a separate chart without adding automated tests.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-05
@@ -0,0 +1,31 @@
## Context
See `proposal.md` for motivation. `TickerService` and `TickerServiceImpl` already exist as empty shells in AssetAZ-owned packages. `AssetPrice` wraps `ΩPriceΩ` and `TradingPair`; the latter identifies currencies structurally rather than by raw symbol. The repository temporarily hosts AssetAZ code as Cauldron, and NenjimHub's current autorun list directly constructs services.
## Goals / Non-Goals
**Goals:** Establish a small reusable ticker contract, deterministic file-backed startup, atomic persist-before-publish behavior, and temporary autorun wiring while retaining AssetAZ ownership.
**Non-Goals:** Datasource plugins, real market data, pairs other than `EVE_USDC`, consumers, subscriptions/callbacks, history queries, retention, compaction, or automated tests.
## Decisions
- Define `PriceObservation` in `com.r35157.assetaz.core.service.ticker` exactly as a record of `AssetPrice price` and `Instant observedAt`. Extend `TickerService` with `start()` and `PriceObservation getLatestPrice(TradingPair tradingPair)`. Use clear exceptions for unsupported/inactive pairs and enabled pairs without a persisted observation; `Optional` and `null` were rejected because issue #60 explicitly requires exceptions for unavailable prices. Raw symbol strings were rejected because callers must use the established ValueTypes.
- Add stable EVE and USDC currency definitions and an `EVE_USDC` well-known `TradingPair`, then construct the sample price with `new ΩPriceΩ("14.85")`. This keeps identity consistent with other typed prices instead of parsing pair names at the API boundary.
- Keep the public API and `PriceObservation` in the AssetAZ API package and implementation details in `com.r35157.assetaz.core.service.ticker.impl.ref`. Their current repository location is temporary Cauldron placement and does not imply Evelyn or NenjimHub ownership.
- Use the fixed path `data/assetaz/ticker/EVE_USDC.prices`; do not create its directory or file. Check existence when initializing the pair. Missing means inactive, emits a warning containing the pair and expected path, and does not prevent the rest of Ticker startup; an existing zero-length, blank-only, or comment-only file is active with no latest value.
- Store one observation per data line as `<uuuuMMddHHmmssSSS'Z'>:<decimal-price>`, using a strict formatter fixed to UTC. Read line by line, remove text from the first `#`, trim, and skip empty results. Split the remaining data at the required colon and parse it into `Instant`, `ΩPriceΩ`, and the fixed `EVE_USDC` pair. Wrap any structural, timestamp, or price failure with the filename, one-based physical line number, and identifying invalid content.
- Load history synchronously during `start()` and select the parsed observation with the greatest `observedAt` timestamp rather than assuming the file is chronologically ordered. After loading, generate the first hardcoded observation immediately, then use a single-thread scheduled executor with fixed-delay scheduling: each subsequent attempt begins one minute after the preceding attempt completes. Fixed-rate scheduling was rejected because a long pause can cause rapid catch-up attempts. Do not schedule generation for an inactive pair. The scheduler is an implementation mechanism, not a public lifecycle or subscription API.
- Serialize generation and publication, and keep the published latest value behind a thread-safe reference. Validate and encode every generated observation, append all encoded bytes through a `FileChannel`, and call `force(true)`. Only after successful persistence, compare its `observedAt` with the current latest and replace the in-memory reference only when the new timestamp is later. Thus concurrent callers see the successfully persisted observation with the greatest timestamp across loaded history and newly persisted observations, and a manually inserted future-dated observation remains latest until a later observation is persisted. A buffered-writer `flush()` alone was rejected because it does not satisfy the issue's explicit flush/force-to-persistence ordering. On open, write, or force failure, log/report the failure and retain the prior latest value; never publish the attempted observation.
- Add a temporary `startAssetAZTickerService()` entry to `NenjimHubImpl` following its direct autorun pattern. It constructs and starts `TickerServiceImpl`; no Ticker dependency is passed to Evelyn, EMC, alarms, portfolios, or other services.
## Risks / Trade-offs
- [Filesystem or device guarantees can still limit crash durability after `FileChannel.force(true)`] → Force each complete appended record before publication and document that the implementation uses the strongest standard Java file-channel guarantee available here.
- [An observation attempt can take arbitrarily long] → Fixed-delay scheduling waits until the attempt completes and then waits one minute, preventing overlapping or catch-up attempts.
- [Malformed history prevents the service from starting] → Include exact filename and line number so operators can repair the activation file safely.
- [Hardcoded identifiers and price are temporary] → Isolate them behind well-known ValueTypes and the reference implementation for later source replacement.
## Migration Plan
Operators opt in by creating `data/assetaz/ticker/EVE_USDC.prices`, optionally pre-populated in the documented format. Deploy the service and temporary autorun wiring together. Rollback removes the autorun call and ticker implementation while leaving the append-only data file intact.
@@ -0,0 +1,29 @@
## Why
AssetAZ has only empty ticker service shells, so callers cannot obtain a typed latest price or retain observations across restarts. A minimal reusable ticker establishes that service contract and a durable reference implementation before real market sources are introduced.
## What Changes
- Define an AssetAZ ticker API that exposes the latest persisted observation by `TradingPair` using existing price ValueTypes and throws clear exceptions when no price is available.
- Add a reference implementation for the single supported `EVE_USDC` pair, producing the hardcoded price `14.85` immediately at startup and starting each subsequent attempt one minute after the previous attempt completes.
- Make an existing `.prices` file the explicit activation mechanism, load its valid history, append every new observation durably, and expose the successfully persisted observation with the greatest timestamp as latest.
- Start the ticker through NenjimHub's current temporary autorun mechanism.
- Leave the pair unavailable when its history file is missing, reject malformed history at startup, and retain the prior published observation when a new observation cannot be persisted safely.
## Capabilities
### New Capabilities
- `assetaz-ticker-service`: Typed latest-price access, file-backed activation and history loading, and durable periodic publication for the initial AssetAZ trading pair.
### Modified Capabilities
None.
## Impact
- Extends the existing empty API and reference implementation under `com.r35157.assetaz.core.service.ticker` and adds `PriceObservation` there.
- Reuses `AssetPrice`, `TradingPair`, and `ΩPriceΩ`; no datasource-plugin or consumer integration is introduced.
- Adds temporary startup wiring to `NenjimHubImpl`, without transferring ownership of the ticker to NenjimHub.
- Uses `data/assetaz/ticker/EVE_USDC.prices` as operator-controlled persistent activation and history.
- Adds no automated tests.
@@ -0,0 +1,69 @@
## Purpose
Provide AssetAZ callers with typed latest prices backed by explicitly enabled, durable observation histories.
## ADDED Requirements
### Requirement: Ticker exposes typed latest prices
The ticker SHALL expose the latest successfully persisted `PriceObservation` for a requested `TradingPair`. A `PriceObservation` SHALL contain exactly an `AssetPrice` and its observation `Instant`; the `AssetPrice` SHALL contain both its `ΩPriceΩ` value and `TradingPair`. The ticker SHALL initially support only `EVE_USDC`. A request for an unsupported or inactive pair, or for an enabled pair that has no persisted observation, SHALL throw a clear exception rather than return `null`.
#### Scenario: Latest supported price is available
- **WHEN** a caller requests `EVE_USDC` after at least one observation has been loaded or published
- **THEN** the ticker returns the latest observation with an `AssetPrice` identified by the `EVE_USDC` `TradingPair`
#### Scenario: Unsupported or inactive pair is requested
- **WHEN** a caller requests a pair that is unsupported or not activated
- **THEN** the ticker throws an exception that clearly identifies the unavailable trading pair
#### Scenario: Enabled pair has no persisted observation
- **WHEN** a caller requests an enabled pair before any observation has been successfully persisted
- **THEN** the ticker throws an exception that clearly states that no persisted price is available for that trading pair
### Requirement: Price history explicitly activates the pair
The ticker SHALL treat the existence of `data/assetaz/ticker/EVE_USDC.prices` when the pair is initialized as activation of `EVE_USDC`. It SHALL load every valid observation and select the observation with the greatest timestamp as latest, regardless of file order. It SHALL accept empty lines, full-line comments whose first non-whitespace character is `#`, and inline comments beginning at the first `#` on a data line. A missing file SHALL produce a warning containing the trading pair and expected filepath, leave the pair unavailable, and SHALL NOT be created automatically; an existing file with no data observations SHALL enable the pair without an initial latest price.
#### Scenario: Existing history activates and restores the pair
- **WHEN** the ticker starts with an existing history containing valid observations and permitted whitespace or comments
- **THEN** `EVE_USDC` is available and the observation with the greatest timestamp is its latest persisted price
#### Scenario: Empty history activates without a price
- **WHEN** the ticker starts with an existing empty history file
- **THEN** `EVE_USDC` is enabled with no initial latest observation
#### Scenario: Missing history leaves the pair unavailable
- **WHEN** the ticker starts without the `EVE_USDC` history file
- **THEN** it logs a warning containing `EVE_USDC` and `data/assetaz/ticker/EVE_USDC.prices`, continues starting without that pair, and does not create the file or its parent directories
#### Scenario: Malformed history is rejected clearly
- **WHEN** comment text is removed and a non-empty data line does not contain a valid UTC timestamp and price
- **THEN** Ticker startup fails with an error containing the history filename, the one-based physical line number, and enough of the invalid content to identify it
### Requirement: History uses the human-editable observation format
Each data line SHALL have the form `<UTC timestamp>:<price>`, where the timestamp uses `uuuuMMddHHmmssSSS'Z'` with millisecond precision in UTC. Automatic writes SHALL append plain data lines only and SHALL leave all existing comments and blank lines untouched.
#### Scenario: Observation is written in the required format
- **WHEN** an observation at `2026-08-05T13:15:42.783Z` with price `14.85` is persisted
- **THEN** the appended line is `20260805131542783Z:14.85`
#### Scenario: Existing operator annotations are preserved
- **WHEN** the ticker appends an observation to a history containing comments or blank lines
- **THEN** those existing lines remain unchanged and the appended observation contains neither a comment nor extra annotation
### Requirement: New observations are durable before publication
For enabled `EVE_USDC`, the ticker SHALL generate price `14.85` immediately at startup. After that startup attempt completes, and after each subsequent attempt completes, the ticker SHALL wait one minute before beginning the next attempt. It SHALL NOT use fixed-rate scheduling or perform catch-up attempts after a delay. For each generated observation it SHALL validate the observation, append it to history, and flush or force the data to persistent storage; every generated observation SHALL be persisted unless that persistence attempt fails. Only after persistence succeeds SHALL the ticker replace the in-memory latest observation, and then only if the newly persisted observation's `observedAt` timestamp is later than the current latest timestamp. If persistence fails, it SHALL report the failure clearly, SHALL NOT publish the attempted observation, and SHALL preserve the previously persisted latest observation. Concurrent reads SHALL never observe an unpersisted price, and latest SHALL always be the successfully persisted observation with the greatest `observedAt` timestamp across loaded history and newly persisted observations.
#### Scenario: Startup observation is persisted
- **WHEN** the ticker starts with an enabled writable history
- **THEN** it appends and forces a `14.85` observation to persistent storage before considering it for latest
#### Scenario: Periodic observation is persisted
- **WHEN** one minute has elapsed after the preceding observation attempt completed while the enabled ticker is running
- **THEN** it begins another attempt and appends and forces its `14.85` observation to persistent storage before considering it for latest
#### Scenario: Future-dated history remains latest
- **WHEN** history contains a successfully persisted observation whose timestamp is later than a newly generated observation
- **THEN** the newly generated observation is still persisted and the future-dated history observation remains latest
#### Scenario: Persistence fails
- **WHEN** appending or flushing a new observation fails
- **THEN** the failure is reported clearly, the failed observation is not published to any reader, and the previously persisted latest observation remains available
@@ -0,0 +1,21 @@
## 1. AssetAZ Ticker API
- [x] 1.1 Add stable EVE and USDC currency ValueTypes and the well-known `EVE_USDC` `TradingPair`.
- [x] 1.2 Add immutable `PriceObservation` with a timestamp and `AssetPrice` in the ticker API package.
- [x] 1.3 Extend `TickerService` with `start()` and `PriceObservation getLatestPrice(TradingPair)`, using clear exceptions for unsupported/inactive pairs and enabled pairs without a persisted observation.
## 2. File-Backed Reference Implementation
- [x] 2.1 Implement explicit activation from the existing `data/assetaz/ticker/EVE_USDC.prices` file without creating a missing file or directory.
- [x] 2.2 Implement the strict UTC `uuuuMMddHHmmssSSS'Z':<price>` format and line-by-line history parsing with empty-line and full-line/inline comment support.
- [x] 2.3 Report malformed data with filename, one-based physical line number, and identifying invalid content, and restore the valid observation with the greatest `observedAt` timestamp as latest.
- [x] 2.4 Implement thread-safe append-and-`FileChannel.force(true)` behavior that persists every generated observation, never exposes an unpersisted observation, preserves the prior latest observation on persistence failure, and replaces latest after successful persistence only when the new `observedAt` timestamp is later.
- [x] 2.5 Generate the hardcoded `14.85` startup observation immediately and use fixed-delay scheduling only for the enabled pair so each subsequent attempt begins one minute after the preceding attempt completes.
## 3. Temporary Autorun Wiring
- [x] 3.1 Start `TickerServiceImpl` from NenjimHub's current direct autorun mechanism without wiring it to any consumer.
## 4. Verification
- [x] 4.1 Compile the project and manually verify missing, empty/comment-only, out-of-order valid, future-dated, malformed, and unwritable history-file behavior, required exception cases, timestamp/price formatting, fixed-delay timing without catch-up attempts, persist-before-publish ordering, and greatest-`observedAt` latest selection without adding automated tests.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-06
@@ -0,0 +1,35 @@
## Context
See `proposal.md` for motivation and the `assetaz-ticker-service` delta for behavior. Nenjim's planned component model constructs every component independently in a context, leaves it initialized but unstarted, and later lets components query that context for peer plugins by interface. Issue #61 therefore requires plugin-style acquisition without constructor-time coupling between a price source and the ticker, while retaining the existing typed ValueTypes, durable append format, and temporary Cauldron placement.
## Goals / Non-Goals
**Goals:** Keep acquisition resources owned by independently constructible sources; make the ticker the source-discovery owner, lifecycle coordinator, and durable sink; isolate histories by stable source identity; preserve greatest-timestamp selection and persist-before-publish under concurrent callbacks; align dependencies with Nenjim's future component context.
**Non-Goals:** Implementing Nenjim's final context-query API in this change, dynamic source changes after ticker startup, automatic migration, source health/retry policy, Raydium or streaming integrations, public history queries, consumers, or automated tests.
## Decisions
- Add API-package `PriceSource` and functional `PriceSink` interfaces. `PriceSource.start(PriceSink)` performs lifecycle-time wiring, so a source constructor needs neither the ticker nor a sink. `TickerService` exposes `start`, `stop`, and latest-price queries but no `addPriceSource`; plugin management is an implementation responsibility rather than a client capability. `TickerService` does not extend `PriceSink`, while `TickerServiceImpl` implements both contracts.
- In the final Nenjim model, `TickerServiceImpl` queries its own context for `PriceSource` implementations after all components have been independently constructed. It does not query for sinks: the ticker is the sink and passes itself to each active source when calling `priceSource.start(this)`. The current public varargs constructor `TickerServiceImpl(PriceSource...)` is temporary Cauldron wiring that supplies exactly the instances a future context lookup will return; it is not public plugin-management API or the final discovery mechanism.
- Add `PriceSourceName` beneath `String -> Name` in `conf/detag.conf`, as explicitly authorized by issue #61. Store source identity in `PriceObservation` but not in history lines, because the source directory is the persistent identity and repeated line metadata would create disagreement risks.
- Represent each context-provided source internally by the source object, its `(TradingPair, sourceName)` key, derived path, activation state, and a per-history lock. Reject duplicate keys and unsafe names while capturing the constructor-provided/discovered set before startup. Compare callbacks by exact source object identity as well as stable metadata so an unknown impersonating instance cannot address another source's history.
- Centralize paths under the ticker data root using canonical base UUID, canonical quote UUID, validated source name, and sanitized symbols in the final filename. Source names are rejected rather than rewritten because they are technical persistent identity. Symbols are non-identity display text and are converted character-by-character to safe filename components, rejecting an empty result.
- During `start()`, validate and derive every obtained source, check exact file existence without scanning or creating, load all active histories into local state, compute per-pair maximum timestamps, then commit startup state and start active sources with `source.start(this)`. If loading or starting fails, stop every source already started and leave the ticker stopped rather than expose a partially started lifecycle.
- During `stop()`, prevent new callbacks, stop every started source, and aggregate/report lifecycle failures while ensuring every source receives a stop attempt. The obtained source set remains owned by the ticker across a subsequent restart; changing the context-provided set dynamically while starting, started, or stopping is outside this design.
- Move the hardcoded price, clock, one-minute delay, and scheduled executor into `HardcodedPriceSource`. Its public constructor has no sink or ticker dependency. `start(PriceSink)` first rejects an already-started call, then stores the supplied sink, announces immediately, and schedules subsequent attempts with `scheduleWithFixedDelay`; checking first prevents a repeated start from replacing the live sink. `stop()` shuts down its executor. The ticker contains none of those acquisition constants or resources.
- On `announce`, resolve the exact active registration, validate non-null price and timestamp, construct `AssetPrice` and `PriceObservation`, lock that source history, append the complete UTF-8 line with `FileChannel`, and call `force(true)`. Only afterward atomically update the pair's latest value when the new timestamp is greater. Per-history locks allow independent sources to persist concurrently without interleaving, while atomic per-pair maximum updates preserve cross-source ordering.
- Keep strict UTC parsing and the existing comment behavior. Loaded observations receive trading pair and source name from the obtained source context. The old flat history path is deliberately ignored; operator-controlled manual movement is the only migration.
- Temporary NenjimHub wiring constructs an unstarted `HardcodedPriceSource` independently, then constructs `TickerServiceImpl` with that source through the varargs Cauldron adapter and starts the ticker. The ticker performs sink wiring and lifecycle. This simulates future context lookup without transferring AssetAZ ownership or adding consumers.
## Risks / Trade-offs
- [A source throws during startup after earlier sources started] → Stop all sources that were successfully started, suppress secondary stop failures, and fail ticker startup clearly.
- [A source callback races with stop] → Guard lifecycle and active-registration checks so callbacks are accepted only while the ticker is started; source stop is responsible for terminating its own producer resources.
- [Multiple source callbacks contend for one trading pair] → Use per-history serialization for files and an atomic greatest-timestamp update for the shared pair view.
- [Symbol sanitization can make filenames less recognizable] → Preserve UUIDs as technical identity and use a deterministic safe replacement only for the human-readable leaf filename.
- [Manual migration can temporarily deactivate the hardcoded source] → Log the exact expected new path and never create or infer content, making operator action explicit and reversible.
## Migration Plan
Before deployment, stop the old service and move `data/assetaz/ticker/EVE_USDC.prices` to `data/assetaz/ticker/019c3f9f-41d1-7a73-b1df-d4c11c7ff301/019c3f9f-41d1-7a73-b1df-d4c11c7ff302/Hardcoded/EVE_USDC.prices`, creating parent directories only as an operator action. Deploy the API, ticker, hardcoded source, Detag configuration, and wiring together. Rollback restores the previous code and moves the file back to the legacy flat path; no automatic data transformation is involved.
@@ -0,0 +1,31 @@
## Why
The AssetAZ Ticker currently owns a hardcoded price generator, so acquisition, scheduling, persistence, and publication are coupled in one implementation. Nenjim's future component model constructs components independently and lets a component obtain peer plugins from its context after construction, so price sources must be independently constructible and wired to the ticker only when the ticker starts them.
## What Changes
- Add `PriceSource` and functional `PriceSink` API contracts in which independently constructed sources receive their sink through `start(PriceSink)` and own their acquisition resources.
- Keep plugin discovery and lifecycle inside `TickerServiceImpl`: it obtains `PriceSource` implementations from its Nenjim context, starts them with itself as sink, and stops them. Ordinary `TickerService` clients receive no plugin-management API.
- Add the `ΩPriceSourceNameΩ` ValueTag and include stable source identity in each in-memory `PriceObservation`.
- **BREAKING** Replace the single history file with an independent UUID-based history path for each obtained `(TradingPair, sourceName)` source identity; the old file is not migrated automatically.
- Refactor `TickerServiceImpl` into a source-agnostic sink that validates callbacks, persists each source independently, and publishes the greatest successfully persisted timestamp across active sources for a pair.
- Move the existing `EVE_USDC = 14.85` generation and fixed-delay scheduler into a stoppable `HardcodedPriceSource` plugin.
- Use a temporary `TickerServiceImpl(PriceSource...)` Cauldron constructor to simulate the future Nenjim context lookup while constructing `HardcodedPriceSource` independently.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `assetaz-ticker-service`: Generalize the ticker from one internal generator and history into context-provided source plugins with per-source activation, persistence, lifecycle, and cross-source latest selection.
## Impact
- Changes the AssetAZ ticker plugin API and reference implementation under `com.r35157.assetaz.core.service.ticker`; the ordinary `TickerService` API exposes lifecycle and price queries, not plugin registration.
- Adds `PriceSourceName` to the shared Detag hierarchy under `String -> Name`; this configuration change is explicitly required by issue #61.
- Changes the operator-managed history location and requires manual movement of the old hardcoded history.
- Updates temporary wiring in `NenjimHubImpl` to construct the source and ticker independently before injecting the source list as a stand-in for future context discovery.
- Adds no Raydium, WebSocket, history-query, retention, or automated-test functionality.
@@ -0,0 +1,122 @@
## ADDED Requirements
### Requirement: Price sources are discovered and lifecycle-managed
Nenjim SHALL be able to construct each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL obtain `PriceSource` plugins from its Nenjim context, while ordinary ticker clients SHALL NOT receive a public operation for adding or managing sources. The ticker SHALL identify each obtained source by its `TradingPair` and stable source name, reject duplicate identities clearly, start each active source by passing itself as `PriceSink`, and stop every source it started. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink.
#### Scenario: Sources are constructed independently
- **WHEN** Nenjim constructs a price source before constructing or starting the ticker
- **THEN** source construction requires neither a `PriceSink` nor a ticker reference and leaves the source unstarted
#### Scenario: Ticker obtains source plugins
- **WHEN** the ticker initializes its source set from its Nenjim context
- **THEN** it looks for implementations of `PriceSource`, not implementations of `PriceSink`, and manages the obtained sources internally
#### Scenario: Ordinary clients cannot manage plugins
- **WHEN** a caller uses the public `TickerService` contract
- **THEN** the contract exposes ticker lifecycle and latest-price queries but no source-registration operation
#### Scenario: Distinct context sources are accepted
- **WHEN** the ticker obtains two sources with different source names or trading pairs from its context
- **THEN** it accepts both and manages each source independently
#### Scenario: Duplicate persistent identity is rejected
- **WHEN** the ticker obtains sources with the same trading pair and source name
- **THEN** initialization fails with an error identifying the duplicate source history
#### Scenario: Unsafe source name is rejected
- **WHEN** a source name is empty or contains a slash, backslash, `..`, or a control character
- **THEN** ticker initialization or startup fails clearly without rewriting the source name or creating filesystem content
#### Scenario: Active source lifecycle is managed
- **WHEN** the ticker starts and later stops with an active context-provided source
- **THEN** it calls `start` with itself as `PriceSink` and subsequently stops the source without leaving source-owned resources running
#### Scenario: Repeated source start preserves existing sink
- **WHEN** an already-started source receives another start call with a different sink
- **THEN** it rejects the call before replacing the sink from the successful start
## MODIFIED Requirements
### Requirement: Ticker exposes typed latest prices
The ticker SHALL expose the latest successfully persisted `PriceObservation` for a requested `TradingPair` across all active context-provided sources for that pair. A `PriceObservation` SHALL contain exactly an `AssetPrice`, its observation `Instant`, and the stable source name; the `AssetPrice` SHALL contain both its `ΩPriceΩ` value and `TradingPair`. If several active sources provide a pair, latest SHALL be the observation with the greatest `observedAt` timestamp, and its source name SHALL identify its source. A request for a pair with no obtained sources, no active sources, or no successfully persisted observation SHALL throw a clear exception rather than return `null`.
#### Scenario: Latest supported price is available
- **WHEN** a caller requests a trading pair after at least one observation has been loaded or persisted by an active source
- **THEN** the ticker returns the successfully persisted observation with the greatest timestamp and its source identity
#### Scenario: Newest observation is selected across sources
- **WHEN** multiple active sources for one trading pair have successfully persisted observations
- **THEN** the ticker returns the observation with the greatest `observedAt` across those sources
#### Scenario: Pair has no obtained or active source
- **WHEN** a caller requests a pair with no context-provided source or whose obtained sources are all inactive
- **THEN** the ticker throws an exception that clearly identifies the unavailable trading pair
#### Scenario: Active pair has no persisted observation
- **WHEN** a caller requests a pair whose active sources have no successfully persisted observation
- **THEN** the ticker throws an exception that clearly states that no persisted price is available for that trading pair
### Requirement: Price history explicitly activates each source
For each context-provided source, the ticker SHALL derive an independent history path as `data/assetaz/ticker/<base UUID>/<quote UUID>/<source name>/<safe base symbol>_<safe quote symbol>.prices`. UUIDs SHALL use their unchanged canonical representation and SHALL be the technical trading-pair identity; sanitized symbols SHALL be used only for the human-readable filename. Only an existing file at that exact path SHALL activate the source. The ticker SHALL neither scan for arbitrary sources nor create or migrate missing files or directories. It SHALL load every valid observation using the obtained source's trading pair and source name and select the greatest timestamp across all active histories for each pair, regardless of file order. Empty lines, full-line comments, and inline comments SHALL remain supported. An existing history with no data SHALL activate only that source without an initial observation, while a missing history SHALL leave only that source inactive and emit a warning containing the trading pair, source name, and complete expected path.
#### Scenario: Existing source history activates and restores identity
- **WHEN** a context-provided source has an existing valid history with permitted whitespace or comments
- **THEN** that source is active and its observations are restored with its captured trading pair and source name
#### Scenario: Empty source history activates without a price
- **WHEN** a context-provided source has an existing empty or comment-only history file
- **THEN** that source is active without an initial observation
#### Scenario: Missing source history leaves only that source inactive
- **WHEN** one obtained source history is missing while another source for the same pair has an existing history
- **THEN** the missing source is not started, the existing source remains active, and no missing path is created
#### Scenario: Missing history warning identifies the source
- **WHEN** a context-provided source's expected history file is absent
- **THEN** the ticker logs a warning containing its trading pair, source name, and complete expected UUID-based path
#### Scenario: Malformed source history is rejected clearly
- **WHEN** comment text is removed and a non-empty data line in an active source history lacks a valid UTC timestamp and price
- **THEN** ticker startup fails with an error containing the history filename, one-based physical line number, and identifying invalid content
#### Scenario: Legacy history is not migrated
- **WHEN** only `data/assetaz/ticker/EVE_USDC.prices` exists
- **THEN** the ticker does not read, move, copy, or create a replacement for that legacy file
### Requirement: History uses the human-editable observation format
Each source history data line SHALL retain the form `<UTC timestamp>:<price>`, where the timestamp uses `uuuuMMddHHmmssSSS'Z'` with millisecond precision in UTC. Trading-pair and source identity SHALL come from the context-provided source and its directory context and SHALL NOT be stored in individual data lines. Automatic writes SHALL append plain data lines only and SHALL leave all existing comments and blank lines untouched.
#### Scenario: Observation is written in the required format
- **WHEN** any source observation at `2026-08-05T13:15:42.783Z` with price `14.85` is persisted
- **THEN** the appended line is `20260805131542783Z:14.85`
#### Scenario: Identity is not duplicated in data lines
- **WHEN** an observation is appended to a source history
- **THEN** its data line contains neither source name nor trading-pair identity
#### Scenario: Existing operator annotations are preserved
- **WHEN** the ticker appends an observation to a source history containing comments or blank lines
- **THEN** those existing lines remain unchanged and the appended observation contains neither a comment nor extra annotation
### Requirement: New observations are durable before publication
Only an active source obtained and started by the ticker SHALL be allowed to announce an observation. For each accepted announcement, the ticker SHALL validate the source, typed price, and timestamp; construct an observation with the source's captured trading pair and source name; serialize writes to that source history; append and force the data to persistent storage; and only after persistence succeeds consider it for the trading pair's in-memory latest value. Concurrent callbacks from different sources SHALL neither corrupt nor interleave writes, and readers SHALL never observe an unpersisted price. Every accepted observation SHALL be persisted even when an existing observation has a later timestamp, and latest SHALL change only when the newly persisted timestamp is later. If persistence fails, the ticker SHALL report the trading pair, source, and path clearly and preserve the prior latest observation.
#### Scenario: Active obtained source observation is persisted
- **WHEN** an active source started by the ticker announces a valid price and timestamp
- **THEN** the ticker appends and forces the observation to that source's history before considering it for latest
#### Scenario: Inactive or unknown source callback is rejected
- **WHEN** an inactive source or a source not obtained by the ticker announces an observation
- **THEN** the ticker rejects the callback without persisting or publishing it
#### Scenario: Future-dated observation remains latest
- **WHEN** an accepted observation is persisted with a timestamp earlier than the current latest observation across active sources
- **THEN** the accepted observation remains in its source history and the current later observation remains latest
#### Scenario: Concurrent source callbacks remain durable
- **WHEN** active sources announce observations concurrently
- **THEN** each successful append is a complete non-interleaved history line and latest identifies the greatest successfully persisted timestamp
#### Scenario: Persistence fails
- **WHEN** appending or forcing an accepted observation fails
- **THEN** the failure identifies the trading pair, source, and path, the attempted observation is not published, and the previously persisted latest observation remains available
@@ -0,0 +1,23 @@
## 1. Ticker Source API
- [x] 1.1 Add `PriceSourceName` under `String -> Name` in `conf/detag.conf` and retain required generated-type imports in `.tjava` sources.
- [x] 1.2 Add the `PriceSource` and functional `PriceSink` API contracts with typed metadata and `PriceSource.start(PriceSink)` lifecycle wiring.
- [x] 1.3 Extend `PriceObservation` with source identity and extend `TickerService` with stop lifecycle while keeping plugin management out of the public service API.
## 2. Source Registry and Persistence
- [x] 2.1 Implement internal handling of context-provided sources with identity validation, duplicate rejection, lifecycle state checks, and exact-instance callback validation in `TickerServiceImpl`.
- [x] 2.2 Implement centralized UUID/source/safe-symbol history paths without scanning, creating, or migrating filesystem content.
- [x] 2.3 Load active per-source histories with strict parsing, comments, source-context observations, clear malformed-line errors, and greatest-timestamp restoration across sources.
- [x] 2.4 Persist concurrent active-source announcements to independent histories with complete-line serialization and `FileChannel.force(true)` before atomic greatest-timestamp publication.
- [x] 2.5 Implement ticker start/stop orchestration so only active sources receive `start(this)`, every started source receives a stop attempt, and partial startup is rolled back.
## 3. Hardcoded Source and Wiring
- [x] 3.1 Add an independently constructible, stoppable `HardcodedPriceSource` whose `start(PriceSink)` preserves an existing sink on repeated start, announces `EVE_USDC = 14.85` immediately, and owns one-minute fixed-delay scheduling.
- [x] 3.2 Update NenjimHub temporary autorun wiring to construct the hardcoded source independently and pass it through `TickerServiceImpl(PriceSource...)` as a Cauldron simulation of future context discovery.
## 4. Verification
- [x] 4.1 Compile the project and manually verify API shape, constructor-independent sources, start-time sink wiring and repeated-start protection, safe and duplicate source handling, missing/empty/malformed histories, UUID paths, legacy-file exclusion, source lifecycle, per-source formatting, persistence failure, concurrent callbacks, and greatest-timestamp selection without adding automated tests.
- [x] 4.2 Validate the OpenSpec change, confirm `TickerServiceImpl` has no generator/scheduler constants, and confirm no unrelated behavior or generated Detag source was edited.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-07
@@ -0,0 +1,62 @@
## Context
See `proposal.md` for motivation. Currency metadata currently lives in `WellKnownCurrencyTypes`, while `WellKnownTradingPairs` captures static pairs and `RaydiumImpl` hardcodes one of them. The repository temporarily hosts both logical AssetAZ API and implementation packages, and Nenjim runtime discovery is outside this issue.
## Goals / Non-Goals
**Goals:**
- Separate the public identity API from its hardcoded implementation package.
- Make UUID identity robust across service instances and metadata revisions.
- Migrate every production caller in one step so no parallel authoritative registry remains.
- Let Raydium translate the pool response's actual mint identities into its returned pair.
**Non-Goals:**
- Moving value types other than `CurrencyType`.
- Runtime service discovery, remote/database catalogues, public registration, or constructor restriction.
- Unit tests or Nenjim Test Tool changes.
## Decisions
### External identity is a private composite key
`ExternalCurrencyReference` is the public immutable carrier and explicitly implements equality and hashing from `namespace + externalId`; optional symbol metadata is excluded. The implementation may retain a private composite key for indexing and conflict diagnostics, but public value equality has the same semantic identity. The hardcoded catalogue separately compares supplied symbol metadata so conflicting catalogue entries are still rejected.
### Canonical and external metadata have different completeness rules
Every `CurrencyType` has a non-null UUID, name, and canonical symbol, even though only its UUID participates in equality. An external reference may omit its observed symbol because integrations can provide a valid namespace and external identifier without display metadata. A currency may also have zero external references; reverse lookup then returns an immutable empty set. Evelyn IOU is not such an example because its Solana mint is configured explicitly.
### Catalogue construction validates before freezing
The hardcoded implementation builds local maps, detects duplicate UUIDs with conflicting metadata and duplicate external identities targeting different UUIDs, then stores immutable copies. Reverse-reference sets are also immutable. This gives lock-free concurrent reads and contains mutation entirely within construction. A public registration API was rejected because version 1 is deliberately read-only.
### NenjimHub owns the service instance
`NenjimHubImpl.startAutoRunProcesses()` is the temporary Cauldron composition root. It creates exactly one `HardcodedCurrencyIdentityService`, then passes the interface-typed instance through constructors to the ticker source, Solana, Jupiter, Raydium, Evelyn, and any other autorun component that needs currency identities. Identity consumers never instantiate the hardcoded implementation themselves and do not use a global singleton. Multiple local service instances were rejected because they create independent lifecycles and prevent Nenjim from replacing the context's implementation coherently.
### Existing UUIDs move with their metadata
The four existing UUIDs move into an API-only `CurrencyTypeIds` constants class. It contains no metadata, mappings, or value instances and is therefore not a currency registry. The hardcoded implementation owns current names, symbols, and external mappings—including the Evelyn IOU name and mint—while production consumers resolve API UUIDs or available external identities through their injected service. No consumer imports `impl.hc`, and no static `CurrencyType` or `TradingPair` objects remain.
### Raydium uses response mint order
`RaydiumImpl` receives `CurrencyIdentityService` beside `SolanaBlockChain`. `fetchPoolPrice` extracts both response mint addresses and optional symbol metadata, creates references in a Solana-mint namespace, resolves each through the service, and constructs `TradingPair(mintA, mintB)`. Missing external symbol fields do not prevent lookup. Other Raydium calculations and State pool accounting likewise resolve actual pool mints and preserve A/B order rather than assuming SOL/SyrupUSDC. Constructor injection is the temporary integration seam until Nenjim provides dynamic dependencies.
## Risks / Trade-offs
- [Existing constructors and imports break during migration] → Update all production call sites and compile every affected Gradle module before completion.
- [Raydium response shape differs between endpoints] → Reuse the existing pool-node extraction path and validate address/symbol fields explicitly with clear IO errors.
- [A second registry survives unnoticed] → Search the complete production tree for old types, static pairs, and legacy imports during final review.
- [No new automated regression coverage] → Respect the explicit issue scope and rely on compilation, strict OpenSpec validation, and focused diff/static review.
## Migration Plan
1. Introduce the AssetAZ API and hardcoded implementation with the preserved catalogue and required Solana mint mappings.
2. Create one implementation instance in NenjimHub's autorun composition root and pass it through every identity-dependent construction chain.
3. Move `CurrencyType`, migrate production imports, and replace static trading-pair use with resolutions from the injected service.
4. Inject the service into Raydium and resolve response mint identities.
5. Delete the legacy registries only after all production references are gone.
6. Compile affected modules without running tests, validate OpenSpec strictly, and inspect the complete diff.
Rollback consists of reverting the change as one unit because the package move, registry deletion, and constructor change are intentionally atomic.
@@ -0,0 +1,29 @@
## Why
Currency identities are currently duplicated in static registries and coupled to callers such as Raydium. AssetAZ needs one service-owned UUID identity model that can resolve external identifiers without making symbols or integrations the canonical identity.
## What Changes
- Add a public AssetAZ Currency Identity Service API and immutable external-reference value type.
- Publish stable AssetAZ currency UUID constants in an API-only identifier class so consumers never depend on the hardcoded implementation package.
- Add a hardcoded reference implementation containing the current currency definitions and Solana mint mappings.
- **BREAKING** Move `CurrencyType` from the shared basic value-types package to `com.r35157.assetaz.valuetypes` and define equality by UUID alone.
- **BREAKING** Remove `WellKnownCurrencyTypes` and `WellKnownTradingPairs`; construct trading pairs from identities resolved by the service.
- Inject the Currency Identity Service into `RaydiumImpl` and resolve pool mint identities when producing prices and ranges.
- Create the hardcoded service exactly once in NenjimHub's autorun composition root and pass that shared service instance to every component that requires currency identities.
- Preserve canonical currency metadata as non-null while allowing external references to omit symbol metadata and allowing currencies to have no external references.
- Do not add or rewrite unit tests or Nenjim Test Tool code as part of this change.
## Capabilities
### New Capabilities
- `assetaz-currency-identity-service`: Defines UUID-based currency identity, external-identifier resolution, the immutable hardcoded catalogue, and its use by Raydium.
### Modified Capabilities
None.
## Impact
The change affects the AssetAZ value-type and service packages, public UUID identifiers, all production imports of `CurrencyType`, NenjimHub dependency wiring, hardcoded ticker data, Raydium construction and pricing, State pool accounting, and callers that construct identity-dependent components. It removes the two legacy static currency/trading-pair registries. No external dependency or ValueTag configuration change is required.
@@ -0,0 +1,112 @@
## Purpose
Provides one authoritative AssetAZ identity model for currencies and translates stable UUIDs and namespaced external identifiers without treating display symbols as identity.
## ADDED Requirements
### Requirement: Stable AssetAZ currency identity
The system SHALL represent a currency as an immutable `CurrencyType` with a non-null UUID, name, and symbol. Its UUID is its complete stable identity. Equality and hash codes SHALL depend only on that UUID, regardless of name, symbol, instance identity, or metadata changes.
#### Scenario: Metadata changes for the same UUID
- **WHEN** two currency values have the same UUID but different names or symbols
- **THEN** they compare equal and have the same hash code
#### Scenario: Matching metadata for different UUIDs
- **WHEN** two currency values have different UUIDs but identical names and symbols
- **THEN** they do not compare equal
#### Scenario: Missing canonical metadata
- **WHEN** construction omits a currency name or symbol
- **THEN** construction fails without producing an incomplete currency value
### Requirement: Resolve canonical currencies
The Currency Identity Service SHALL resolve a known AssetAZ UUID to a non-null current currency value and SHALL fail with a clear exception for an unknown UUID.
#### Scenario: Resolve known UUID
- **WHEN** a client resolves a configured AssetAZ currency UUID
- **THEN** the service returns the current currency metadata for that UUID
#### Scenario: Resolve unknown UUID
- **WHEN** a client resolves an unconfigured UUID
- **THEN** the service throws an exception that clearly identifies the unknown UUID
### Requirement: Resolve namespaced external identities
The Currency Identity Service SHALL resolve external currencies by the pair `namespace + externalId`. External symbols MAY be absent and SHALL be retained as metadata when present, but SHALL NOT participate in lookup identity or equality. A namespace SHALL describe the external identification system rather than an observing price source.
#### Scenario: Resolve a Solana mint
- **WHEN** a client supplies a configured Solana-mint namespace and mint address with symbol metadata
- **THEN** the service returns the currency mapped to that namespace and mint address
#### Scenario: Symbol differs from configured metadata
- **WHEN** the namespace and external identifier are configured but the supplied symbol differs
- **THEN** the external references compare equal and the service resolves the same currency because the symbol is not part of external identity
#### Scenario: External symbol is absent
- **WHEN** a client supplies a configured namespace and external identifier without symbol metadata
- **THEN** the service resolves the currency and returns its non-null canonical symbol
#### Scenario: Unknown external identity
- **WHEN** a client supplies an unconfigured namespace and external identifier
- **THEN** the service throws an exception that clearly identifies the unknown external identity
### Requirement: Reverse external-reference lookup
The Currency Identity Service SHALL return all configured external references for a known AssetAZ UUID as a non-null immutable set, including an empty set when none exist, and SHALL fail clearly for an unknown UUID.
#### Scenario: Currency has multiple references
- **WHEN** a known UUID has references in multiple external systems
- **THEN** reverse lookup returns every reference without exposing mutable registry state
#### Scenario: Known currency has no references
- **WHEN** a known UUID has no configured external references
- **THEN** reverse lookup returns an empty immutable set
#### Scenario: Reverse lookup Evelyn IOU
- **WHEN** a client requests external references for the Evelyn IOU UUID
- **THEN** the result contains its configured Solana mint reference
### Requirement: Immutable conflict-free hardcoded catalogue
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. NenjimHub SHALL create exactly one hardcoded service instance for its autorun context and SHALL pass that same instance to every autorun component that requires currency identities.
#### Scenario: Conflicting external mapping
- **WHEN** initialization maps the same namespace and external identifier to two different AssetAZ UUIDs
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Conflicting currency metadata
- **WHEN** initialization provides conflicting current values for the same UUID
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Compose autorun components
- **WHEN** NenjimHub constructs its autorun component graph
- **THEN** it creates one hardcoded Currency Identity Service and injects that same instance into every component that requires it
### Requirement: Trading pairs use service-owned identities
Production code SHALL construct trading pairs from currency values obtained from the current Currency Identity Service and SHALL NOT maintain a parallel static registry of canonical currency or trading-pair instances.
#### Scenario: Construct a configured pair
- **WHEN** production code needs a trading pair for configured currencies
- **THEN** it resolves both UUIDs or external identities through the current service before constructing the pair
### Requirement: Stable UUID identifiers are implementation-independent
The API SHALL expose stable AssetAZ currency UUID constants without names, symbols, external mappings, or `CurrencyType` instances. Consumers that require a known AssetAZ UUID SHALL use these API identifiers and SHALL NOT import the hardcoded Currency Identity Service implementation.
#### Scenario: Consumer resolves a known UUID
- **WHEN** a consumer needs a known AssetAZ currency by UUID
- **THEN** it obtains the UUID from the API identifier class and resolves it through its injected Currency Identity Service
### Requirement: Pool amounts follow actual mint identities
Pool accounting SHALL resolve mint A and mint B through the Currency Identity Service using their actual namespaced Solana mint addresses and SHALL preserve the pool's A/B order when assigning currency types to amounts.
#### Scenario: State loads a pool
- **WHEN** State receives pool information with mint A and mint B
- **THEN** amount A uses the currency resolved from mint A and amount B uses the currency resolved from mint B
### Requirement: Raydium resolves response currencies
Raydium SHALL receive a Currency Identity Service dependency and SHALL build a fetched pool price's trading pair in Raydium response order by resolving `mintA` and `mintB` as namespaced Solana mint identities. It SHALL NOT resolve currencies by symbol alone.
#### Scenario: Fetch a configured Raydium pool price
- **WHEN** Raydium returns a price plus configured mint A and mint B identifiers, with or without symbols
- **THEN** the returned asset price uses a trading pair whose base is the service resolution of mint A and whose quote is the service resolution of mint B
#### Scenario: Raydium returns an unknown mint
- **WHEN** either returned mint identity is not configured
- **THEN** pool-price creation fails with the Currency Identity Service's clear unknown-identity exception
@@ -0,0 +1,35 @@
## 1. Currency Identity API and Catalogue
- [x] 1.1 Move `CurrencyType` into the AssetAZ value-types package and make equality and hashing UUID-only.
- [x] 1.2 Add the annotated immutable `ExternalCurrencyReference` and read-only `CurrencyIdentityService` API.
- [x] 1.3 Implement the immutable hardcoded catalogue with preserved UUIDs, reverse lookup, Solana mint mappings, and construction-time conflict detection.
## 2. Production Migration
- [x] 2.1 Migrate every production `CurrencyType` import and replace hardcoded ticker pair construction with identities from the current service.
- [x] 2.2 Inject the Currency Identity Service into `RaydiumImpl` and resolve Raydium mint identities and response-ordered trading pairs.
- [x] 2.3 Update production construction/configuration sites for the new dependencies and remove `WellKnownCurrencyTypes` and `WellKnownTradingPairs`.
## 3. Verification
- [x] 3.1 Compile every affected module without running unit tests.
- [x] 3.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 3.3 Review the complete diff and production tree for incomplete migrations, old imports, parallel currency registries, and unintended test or Nenjim Test Tool changes.
## 4. Composition-Root Correction
- [x] 4.1 Create the hardcoded Currency Identity Service exactly once in `NenjimHubImpl.startAutoRunProcesses()` and pass it into every autorun construction chain that needs it.
- [x] 4.2 Replace identity consumers' local hardcoded-service construction with required `CurrencyIdentityService` constructor dependencies.
- [x] 4.3 Compile main and test source sets, strict-validate OpenSpec, run `git diff --check`, and confirm no hardcoded service construction remains outside the NenjimHub composition root.
## 5. Public API Documentation
- [x] 5.1 Document the public Currency Identity Service, external-reference, and currency value APIs, including identity semantics, nullability, and failure behavior.
- [x] 5.2 Compile main and test source sets and rerun strict OpenSpec validation and `git diff --check`.
## 6. Review Corrections
- [x] 6.1 Require complete canonical currency metadata, add API-owned UUID identifiers, and preserve the Evelyn IOU name and Solana mint mapping.
- [x] 6.2 Make external-reference equality use namespace plus external ID while retaining optional symbols and catalogue conflict detection.
- [x] 6.3 Resolve State pool currencies from actual mint A/B identities and make Raydium symbol extraction optional.
- [x] 6.4 Remove consumer dependencies on `impl.hc`, compile without tests, strict-validate OpenSpec, run `git diff --check`, and review the complete migration.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-08
@@ -0,0 +1,78 @@
## Context
See `proposal.md` for motivation. Commit `2fc553b` established package ownership that anticipates later extraction into Nenjim modules while the code remains in one Gradle project. The existing Raydium Pool PriceSource is an unimplemented scaffold in its intended implementation package. The Ticker already activates registered sources only when their exact history file exists and persists callbacks before updating its in-memory latest observation.
## Goals / Non-Goals
**Goals:**
- Complete the existing pool-source scaffold as an independently constructible, restartable PriceSource.
- Preserve the package and future artifact dependency direction introduced by `2fc553b`.
- Compose one EVE/USDT pool source beside the existing hardcoded EVE/USDC source using today's temporary Cauldron wiring.
- Make timing deterministic enough for verification through package-private clock and delay injection.
**Non-Goals:**
- Dynamic Nenjim discovery, configuration files, streaming, backoff, health APIs, history creation or migration, visualization, currency conversion, or further physical module splitting.
- Changes to the generic Ticker behavior or canonical Ticker specification.
## Decisions
### Preserve future module and package ownership
The future boundaries remain:
```text
Ticker API
com.r35157.assetaz.services.ticker
- TickerService
- PriceObservation
PriceSource plugin API
com.r35157.assetaz.services.ticker.plugins.pricesource
- PriceSource
- PriceSink
Ticker reference implementation
com.r35157.assetaz.services.ticker.impl.ref
-> Ticker API
-> PriceSource plugin API
Hardcoded PriceSource implementation
com.r35157.assetaz.services.ticker.plugins.pricesource.impl.hardcoded
-> PriceSource plugin API
Raydium Pool PriceSource implementation
com.r35157.assetaz.services.ticker.plugins.pricesource.impl.raydiumpool
-> PriceSource plugin API
-> Raydium API and value types
```
`PriceSink` stays in the plugin API so implementations do not depend on the Ticker API or reference implementation. `TickerServiceImpl` implements `PriceSink`, owns source lifecycle, and temporarily receives sources through its varargs constructor. `NenjimHubImpl` remains the temporary composition root; dynamic discovery is deferred. The alternative of moving the sink or source packages would recreate future circular or implementation dependencies and is explicitly rejected.
### Use a source-owned single-thread fixed-delay scheduler
Each source creates a daemon single-thread scheduled executor when started. The immediate attempt is submitted to that executor with zero initial delay, followed by fixed-delay scheduling so a slow or paused call cannot produce catch-up observations. A package-private constructor injects `Clock`, delay, and time unit; the public constructor fixes UTC and one minute. A shared scheduler was rejected because lifecycle and termination must belong to the source instance.
### Contain each attempt at the scheduler boundary
One safe polling method catches retrieval, validation, and sink-publication failures, logs pool and pair context, and returns normally so fixed-delay scheduling continues. Pair mismatch is treated as an invalid observation rather than a fatal lifecycle error. The timestamp is read only after a successful matching response and truncated to milliseconds before the sink callback.
### Keep startup and shutdown state synchronized
Startup checks the running state before validating or assigning the new sink, creates fresh scheduling resources, and schedules the immediate task. Stop detaches the scheduler under synchronization, requests immediate shutdown, and waits for termination without holding the lifecycle monitor. Interrupted waiting restores the caller's interrupt flag and ensures shutdown remains requested. This avoids replacing an active sink and permits restart after resource termination.
### Resolve EVE/USDT through the shared Currency Identity Service
The API identifier class gains only a stable USDT UUID. The hardcoded catalogue owns canonical metadata and the official Solana mint mapping. NenjimHub constructs the expected pair by resolving EVE and USDT through its single shared CIS instance, constructs Raydium with that same CIS dependency, and supplies both hardcoded and Raydium sources to the temporary Ticker constructor. The pool ID is market/source identity and is not registered in CIS.
## Risks / Trade-offs
- [A blocking Raydium call can delay shutdown until the call honors interruption] → request immediate shutdown, preserve interruption, and keep the polling thread daemonized.
- [A missing manually provisioned history silently leaves the new source inactive] → retain the existing explicit warning and document the exact pool-based history path.
- [Temporary constructor injection can be mistaken for permanent discovery] → document it as Cauldron wiring and preserve interfaces and packages for later Nenjim discovery.
- [A response may reverse or otherwise change the configured pair] → validate exact pair equality and publish nothing on mismatch.
## Migration Plan
Add USDT identity data, complete the existing source, and extend only the NenjimHub composition graph. Deployment requires manually creating the exact empty EVE/USDT pool history file to activate polling. Rollback removes that file or removes the temporary source wiring; existing hardcoded EVE/USDC history is unchanged.
@@ -0,0 +1,24 @@
## Why
AssetAZ Ticker currently has only a hardcoded price source and therefore cannot observe Evelyn IOU's real Raydium market price. A pool-specific plugin is needed to poll the EVE/USDT pool while preserving the PriceSource lifecycle and future Nenjim module boundaries.
## What Changes
- Complete the existing Raydium Pool PriceSource scaffold as a lifecycle-managed, fixed-delay polling source representing one Raydium pool.
- Add Tether USD (USDT) as a stable, externally resolvable AssetAZ currency distinct from USDC.
- Temporarily compose the EVE/USDT Raydium source in NenjimHub alongside the existing hardcoded EVE/USDC source.
- Preserve history activation, persistence-before-publication, package ownership, and future artifact boundaries without adding discovery, configuration, streaming, conversion, or automatic history creation.
## Capabilities
### New Capabilities
- `assetaz-raydium-pool-price-source`: Defines pool identity, polling, validation, publication, failure isolation, and restartable lifecycle behavior for a Raydium-backed PriceSource.
### Modified Capabilities
- `assetaz-currency-identity-service`: Adds the stable canonical USDT identity and official Solana mint mapping while keeping it distinct from USDC.
## Impact
The change affects the AssetAZ Currency Identity Service catalogue and identifiers, the existing Raydium Pool PriceSource implementation scaffold, and temporary NenjimHub composition. It uses the existing Raydium and PriceSource APIs and does not change the generic Ticker service contract or create new Gradle subprojects.
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Stable Tether USD identity
The Currency Identity Service SHALL provide Tether USD as a stable AssetAZ currency with canonical name `Tether USD`, canonical symbol `USDT`, and an implementation-independent UUID identifier. It SHALL resolve that currency through both the stable UUID and official Solana mint `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB`. USDT and USDC SHALL remain distinct currency identities, and resolution SHALL NOT imply conversion between them.
#### Scenario: Resolve USDT by stable UUID
- **WHEN** a client resolves the stable USDT UUID
- **THEN** the service returns the canonical Tether USD currency with symbol USDT
#### Scenario: Resolve USDT by official Solana mint
- **WHEN** a client resolves the official USDT mint in the Solana-mint namespace
- **THEN** the service returns the same canonical USDT identity as UUID resolution
#### Scenario: Compare USDT and USDC
- **WHEN** a client resolves both USDT and USDC
- **THEN** they have different UUIDs and no implicit conversion is performed
@@ -0,0 +1,76 @@
## Purpose
Provides independently lifecycle-managed price observations for one Raydium pool through the AssetAZ PriceSource plugin contract.
## ADDED Requirements
### Requirement: One source represents one stable Raydium pool
A Raydium Pool PriceSource instance SHALL represent exactly one Raydium pool, SHALL expose the expected trading pair supplied at construction, and SHALL expose the stable source name `Raydium-<poolId>`. Construction SHALL validate all dependencies and identity values, leave the source stopped, perform no network access, and require no price sink until startup.
#### Scenario: Construct a pool source
- **WHEN** a client constructs a source with a Raydium service, pool ID, and expected trading pair
- **THEN** the stopped source reports that trading pair and a source name containing that exact pool ID without performing a price retrieval
#### Scenario: Reject invalid construction
- **WHEN** a required constructor argument is absent or invalid
- **THEN** construction fails without starting polling or accessing the network
### Requirement: Poll immediately with fixed delay
On startup, the source SHALL begin its first retrieval attempt immediately and SHALL begin each subsequent attempt one minute after the preceding attempt completes. Polling SHALL execute on a daemon scheduler thread owned exclusively by that source instance.
#### Scenario: Start a source
- **WHEN** a stopped source is started with a price sink
- **THEN** its first pool-price retrieval begins immediately on its source-owned daemon scheduler
#### Scenario: Complete a periodic attempt
- **WHEN** a retrieval attempt completes
- **THEN** the next attempt begins after the configured one-minute delay without fixed-rate catch-up attempts
### Requirement: Validate and publish pool observations
The source SHALL retrieve the configured pool price and compare the response trading pair with its expected pair. For a matching response, it SHALL create the observation timestamp after the successful response, truncate it to milliseconds, and publish the returned price value through the sink supplied at startup.
#### Scenario: Receive the expected pair
- **WHEN** a successful pool response contains the expected trading pair
- **THEN** the source publishes its price with a post-response timestamp truncated to milliseconds
#### Scenario: Receive a different pair
- **WHEN** a pool response contains a trading pair different from the expected pair
- **THEN** the source publishes no observation, logs the pool ID plus expected and received pairs, and schedules the next attempt normally
### Requirement: Isolate temporary polling failures
Temporary network, Raydium, parsing, interruption, or downstream persistence failures SHALL be logged with the pool ID and expected trading pair and SHALL NOT cancel later polling attempts or stop the Ticker service.
#### Scenario: A polling attempt fails temporarily
- **WHEN** a retrieval or publication attempt throws an exception
- **THEN** the source logs the contextual failure and retries on the next fixed-delay interval
### Requirement: Restartable source lifecycle
The source SHALL reject a repeated startup before replacing its existing sink. Stopping SHALL be idempotent, terminate source-owned scheduling resources, correctly preserve interruption, and permit a later startup with a newly supplied sink.
#### Scenario: Start an already-started source
- **WHEN** startup is invoked while the source is already running
- **THEN** startup fails and the currently active sink remains unchanged
#### Scenario: Stop a running source
- **WHEN** stop is invoked on a running source
- **THEN** its scheduler terminates and no further polling begins
#### Scenario: Stop an already-stopped source
- **WHEN** stop is invoked while the source is stopped
- **THEN** it completes without error
#### Scenario: Restart a stopped source
- **WHEN** a previously stopped source is started with a sink
- **THEN** it creates fresh source-owned scheduling resources and begins an immediate retrieval attempt
### Requirement: Activation uses independent Ticker history
The pool source SHALL participate in the existing Ticker activation contract under its exact trading pair and pool-based source name. It SHALL be active only when that exact history file already exists and SHALL NOT create or migrate history files.
#### Scenario: Pool history exists
- **WHEN** the EVE/USDT history file exists beneath the EVE UUID, USDT UUID, and exact pool-based source-name path
- **THEN** the Ticker activates the Raydium source independently of the hardcoded EVE/USDC history
#### Scenario: Pool history is absent
- **WHEN** the exact Raydium source history file is absent
- **THEN** the Ticker leaves that source inactive and creates no file or directory
@@ -0,0 +1,27 @@
## 1. Currency Identity
- [x] 1.1 Add the API-owned stable USDT UUID and hardcoded canonical Tether USD catalogue entry with the official Solana mint.
- [x] 1.2 Verify USDT resolution by UUID and mint, distinct identity from USDC, and absence of conversion or parallel registry data.
## 2. Raydium Pool PriceSource
- [x] 2.1 Complete the existing `impl.raydiumpool.RaydiumPoolPriceSource` scaffold with validated pool identity and no construction-time sink or network access.
- [x] 2.2 Implement immediate source-owned daemon polling with one-minute fixed delay, response-pair validation, millisecond observation timestamps, sink publication, and contextual failure isolation.
- [x] 2.3 Implement repeated-start rejection, idempotent resource-terminating stop, interruption handling, and restart with fresh scheduling resources.
## 3. Temporary Composition
- [x] 3.1 Construct the EVE/USDT pair and Raydium pool source in `NenjimHubImpl` using the shared Currency Identity Service and hardcoded pool ID.
- [x] 3.2 Inject the Raydium source alongside the unchanged running `HardcodedPriceSource` through the temporary Ticker constructor.
## 4. Boundary and Behavior Review
- [x] 4.1 Verify the `2fc553b` package and future module boundaries: Ticker API, PriceSource/PriceSink plugin API, Ticker reference implementation, and separate hardcoded and Raydium source implementations.
- [x] 4.2 Verify exact pool-based source naming and EVE/USDT history path, existing-file-only activation, persistence before publication, polling recovery, and stop/restart semantics without adding automated tests.
- [x] 4.3 Review the complete diff for stale imports, duplicate implementations, incomplete wiring, unintended HardcodedPriceSource changes, and excluded features.
## 5. Build and Specification Verification
- [x] 5.1 Compile main and test source sets without running unit tests.
- [x] 5.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 5.3 Sync both deltas into canonical specifications, archive the completed change, and rerun strict OpenSpec validation.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-08
@@ -0,0 +1,50 @@
## Context
See `proposal.md` for motivation. Evelyn currently returns hardcoded history and EMC copies it once while constructing six static series. The existing Ticker already supplies persisted latest EVE/USDT observations through its public API. The baseline deliberately disables all NenjimHub autorun services, so this change must explicitly reactivate only the dependency chain needed for the live chart.
## Goals / Non-Goals
**Goals:**
- Keep financial calculation, sampling, and process-local history in Evelyn-owned code.
- Keep JavaFX chart mutation and visualization lifecycle in EMC-owned code.
- Make formula and scheduler behavior deterministic using explicit instants, an injected clock, and injectable delay units.
- Preserve all six typed index fields while enabling only the price-index chart series.
- Retain the safety posture of the baseline composition root.
**Non-Goals:**
- Ticker/Raydium contract changes, index persistence/backfill, other five index calculations, conversions, configuration, streaming, trading, alarms, unrelated service activation, or automated unit-test coverage in this change.
## Decisions
### Separate the pure formula from sampling
An Evelyn-owned calculator accepts explicit `BigDecimal` prices and `Instant` values. It uses the Copenhagen start configuration resolved to the fixed instant `2026-07-31T22:00:00Z`, a 365-day seconds denominator, and an explicit high-precision `MathContext`. Fractional exponentiation is isolated behind the calculator rather than embedded in JavaFX or scheduler code. This makes all reference examples deterministic. Direct calculation in EMC was rejected because presentation must not own financial behavior.
### Make sampling part of the Evelyn lifecycle
The Evelyn API exposes start and stop operations for the complete Evelyn lifecycle rather than exposing sampling controls to presentation code. The reference implementation receives only public `TickerService`, the canonical expected trading pair, a clock, and delay configuration. Starting each instance creates its daemon single-thread scheduler using fixed delay with zero initial delay. Successful attempts append under a lock; snapshot reads use `List.copyOf` under the same lock. Exceptions are caught at the task boundary so future attempts survive. Stopping is idempotent, releases the owned executor, and discards that instance's process-local history. A global scheduler, an EMC-owned scheduler, or shared Production/Test history was rejected because environments and lifecycles must remain independent.
### Let EMC incrementally reconcile snapshots on the JavaFX thread
Each environment chart keeps its own enabled series and a cache used only to optimize rendering. Opening or reopening EMC reconstructs both charts and immediately reconciles each complete authoritative Evelyn snapshot before starting a JavaFX `Timeline` for incremental refresh. Prefix mismatches also force a complete series rebuild, so a stale count cannot hide replaced history. Empty and single-point histories receive non-degenerate timestamp bounds; Y bounds use only absolute Evelyn Price Index values and fall back to a symmetric default. The five existing series-add and bound-contribution statements remain commented scaffolding. Background observer callbacks were rejected because they complicate thread ownership and can mutate JavaFX objects unsafely.
### Keep EMC lifecycle presentation-only
NenjimHub starts both Evelyn instances after Ticker and before opening EMC. EMC only reads their histories and owns its JavaFX refresh timeline. Hiding the window stops that timeline but leaves Evelyn sampling and history untouched. Reopening the same EMC instance creates fresh chart state from the complete current snapshots, so points collected while hidden remain visible. Evelyn sampling stops and its in-memory history is discarded only when the corresponding Evelyn instance is stopped.
### Enable only the required NenjimHub dependency chain
`NenjimHubImpl` creates one CIS, Solana implementation, Raydium implementation, the existing pool-specific EVE/USDT PriceSource, and Ticker; it starts Ticker before constructing and starting two Evelyn instances with the public Ticker and canonical pair, then starts EMC. The hardcoded source, alarm, Composer, Process Manager, Test Tool, Soda Task Manager, Suwimo Client, and all trading behavior remain disabled. Existing-file-only Ticker activation remains unchanged.
## Risks / Trade-offs
- [Fractional exponentiation ultimately uses finite floating-point transcendental support] → isolate conversion and retain DECIMAL128 arithmetic around it; the deterministic seam remains available for later focused verification.
- [EMC refresh can observe several newly sampled points at once] → reconcile authoritative snapshots against the rendered prefix on the JavaFX thread and preserve oldest-to-newest order.
- [Ticker has no active EVE/USDT history or has not completed its first poll] → log and skip the point; fixed-delay sampling retries without fabricating data.
- [EMC can be hidden while samples continue] → keep history authoritative in Evelyn and reconstruct fresh chart state from complete snapshots whenever EMC is reopened.
## Migration Plan
Deploy with the existing manually activated EVE/USDT Ticker history file. On process start, histories are intentionally empty. Rollback restores the disabled autorun composition and static presentation; no data migration is needed because index history is never persisted.
@@ -0,0 +1,26 @@
## Why
Evelyn Mission Control currently renders a static six-series chart from hardcoded data, so it does not describe Evelyn's live market-price balance. Evelyn needs to calculate and retain the first real status index from AssetAZ Ticker while EMC updates safely during the current process lifetime.
## What Changes
- Replace hardcoded Evelyn status-index points with separate, thread-safe, in-memory Production and Test histories sampled for the complete lifecycle of each started Evelyn instance.
- Calculate the expected exponential EVE price and Evelyn Price Index deterministically from an explicit sampling instant and the latest canonical EVE/USDT Ticker observation.
- Isolate temporary Ticker/sample failures so no fake point is created and later fixed-delay samples continue.
- Make EMC's Production and Test charts reconstruct the complete current Evelyn snapshots when opened or reopened and update live on the JavaFX Application Thread, with only Evelyn Price Index enabled and controlling symmetric bounds.
- Compose only CIS, Solana, Raydium, the real EVE/USDT PriceSource, Ticker, Evelyn, and EMC in NenjimHub; leave all unrelated and safety-sensitive services disabled.
- Keep calculation, timing, sampling-delay, and chart-update seams deterministic so focused automated verification can be added later; unit tests are deliberately deferred from this change.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `evelyn-status-index-history`: Replaces hardcoded six-line snapshots with live in-memory Evelyn Price Index sampling, formula, failure handling, separate environment histories, and one enabled live chart series.
## Impact
The change affects the Evelyn API/reference implementation, EMC chart lifecycle and rendering, and temporary NenjimHub composition. It depends on the existing public Ticker and canonical currency identities; generic Ticker and Raydium PriceSource contracts and persistence remain unchanged.
@@ -0,0 +1,71 @@
## ADDED Requirements
### Requirement: Evelyn calculates the live price index
Evelyn SHALL obtain the actual EVE/USDT price from the latest AssetAZ Ticker observation for the canonical EVE/USDT trading pair. It SHALL treat USDT as USD for this calculation and SHALL NOT access Raydium directly. At sampling instant `t`, on or after `2026-07-31T22:00:00Z`, the expected price SHALL be `15 × 1.20^(elapsedSeconds / 31,536,000)` and the Evelyn Price Index SHALL be `((actualPrice / expectedPrice) - 1) × 10`, calculated with adequate decimal precision and without arbitrary history rounding.
#### Scenario: Calculate expected price at the configured start
- **WHEN** the sampling instant is `2026-07-31T22:00:00Z`
- **THEN** the expected EVE price is exactly 15 USD
#### Scenario: Calculate expected price after one year
- **WHEN** exactly 31,536,000 seconds have elapsed since the configured start
- **THEN** the expected EVE price is exactly 18 USD
#### Scenario: Calculate representative fractional-year growth
- **WHEN** 18 days, 3 hours, 8 minutes, and 14 seconds have elapsed
- **THEN** the expected EVE price is approximately `15.136464436276` USD
#### Scenario: Calculate index from actual price
- **WHEN** actual price equals expected price, is 10 percent above, is 10 percent below, is double, or is half the expected price
- **THEN** the respective index is 0, 1, -1, 10, or -5
### Requirement: Evelyn samples without overlapping or catch-up attempts
Each started Evelyn environment SHALL own its sampling executor, attempt its first sample immediately, and make subsequent attempts approximately one minute after the preceding attempt completes. Sampling SHALL remain active for the Evelyn lifecycle independently of whether Mission Control is open. Evelyn SHALL add exactly one timestamped point per successful attempt and SHALL isolate a failed or temporarily unavailable Ticker lookup without adding a fake point, terminating Evelyn, or cancelling the next attempt. Stopping Evelyn SHALL terminate its sampling executor.
#### Scenario: Initial Ticker data is unavailable
- **WHEN** the immediate sampling attempt cannot obtain a latest EVE/USDT price
- **THEN** no point is added, diagnostic context is logged, and the next fixed-delay attempt remains scheduled
#### Scenario: A later sample succeeds
- **WHEN** a later attempt obtains a latest EVE/USDT price
- **THEN** exactly one point is appended using the sampling instant for both expected-price calculation and point timestamp
## MODIFIED Requirements
### Requirement: Evelyn provides historical status-index measurements
Each Evelyn instance SHALL begin with an empty, process-local in-memory history and expose a safe, non-null snapshot of typed measurement points ordered from oldest to newest while sampling may continue concurrently. Each successful point SHALL contain its sampling timestamp, the calculated Evelyn Price Index, and `BigDecimal.ZERO` placeholders for EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index. Production and Test SHALL use separate history collections, and no point SHALL be persisted, restored, or backfilled.
#### Scenario: Status-index history is requested during sampling
- **WHEN** a caller requests an Evelyn environment's status-index history while points may be appended
- **THEN** it receives a safe, non-null, oldest-to-newest snapshot containing all six typed fields
#### Scenario: A process starts two Evelyn environments
- **WHEN** Production and Test Evelyn instances are created for a new process
- **THEN** both histories start empty and remain separate even if their calculated values are identical
#### Scenario: A real point is sampled
- **WHEN** Evelyn appends a successful price-index point
- **THEN** its Evelyn Price Index contains the calculated value and each of the other five fields contains zero
#### Scenario: An Evelyn instance is stopped
- **WHEN** a Production or Test Evelyn instance is stopped
- **THEN** its sampling executor terminates and its complete process-local history is discarded without affecting the other environment
### Requirement: Evelyn Mission Control visualizes status-index measurements
Evelyn Mission Control SHALL display separate live Production and Test Overview charts from their corresponding authoritative Evelyn snapshots. Each chart SHALL contain one enabled series named `Evelyn Price Index`; the five future series and their typed fields SHALL remain available as disabled source scaffolding. Opening or reopening EMC SHALL immediately reconstruct each chart from the complete current snapshot before periodically refreshing it. Successful points SHALL be added to chart objects only on the JavaFX Application Thread while the window remains open. Timestamp bounds SHALL expand as points arrive, and only enabled Evelyn Price Index values SHALL determine a dynamic Y-axis range that is symmetric around and always displays zero, using a reasonable default range for empty or all-zero history. Hiding or closing EMC SHALL release only EMC's JavaFX refresh resources and SHALL NOT start, stop, or clear either Evelyn environment.
#### Scenario: Production and Test Overviews start empty
- **WHEN** Mission Control opens with two newly created Evelyn environments
- **THEN** it displays two separate empty one-series charts with valid timestamp axes and symmetric Y-axes containing zero
#### Scenario: A successful point appears while EMC is open
- **WHEN** an Evelyn history receives a new successful point
- **THEN** the corresponding chart adds it on the JavaFX Application Thread and updates its timestamp and symmetric price-index bounds without reopening EMC
#### Scenario: EMC reopens after collecting hidden-window samples
- **WHEN** the same EMC instance is reopened after Evelyn collected additional points while its window was hidden
- **THEN** each chart is reconstructed immediately from its environment's complete current history without missing points because of stale rendering state
#### Scenario: Future fields contain larger values
- **WHEN** any disabled future-index field has a greater absolute value than Evelyn Price Index
- **THEN** that disabled value does not affect the visible Y-axis bounds
@@ -0,0 +1,24 @@
## 1. Evelyn Calculation and History
- [x] 1.1 Add the deterministic expected-price and Evelyn Price Index calculator with explicit precision and configured start instant.
- [x] 1.2 Replace hardcoded history with an empty thread-safe per-instance snapshot history and preserve all six typed fields using zero placeholders.
- [x] 1.3 Make restartable fixed-delay sampling and complete process-local history owned by each Evelyn lifecycle, with immediate first attempt, failure isolation, clean stop, and history discard on stop.
## 2. Live Mission Control Chart
- [x] 2.1 Refactor Production and Test Overview charts into separate live chart states that reconstruct the complete authoritative snapshot on every open and reconcile later snapshots on the JavaFX thread.
- [x] 2.2 Enable only Evelyn Price Index, retain the five future series and bound contributions as commented scaffolding, and calculate valid dynamic X and symmetric Y bounds from enabled data only.
- [x] 2.3 Keep EMC lifecycle presentation-only: hiding stops its JavaFX refresh, reopening rebuilds fresh chart state, and EMC never starts or stops Evelyn sampling.
## 3. Safe NenjimHub Composition
- [x] 3.1 Enable only CIS, Solana, Raydium, the EVE/USDT Raydium PriceSource, Ticker, two Evelyn environments, and EMC in the composition root.
- [x] 3.2 Start Ticker before constructing and independently starting both Evelyn lifecycles; pass public TickerService plus the canonical CIS-resolved EVE/USDT pair to each instance before starting EMC.
- [x] 3.3 Confirm HardcodedPriceSource and every unrelated alarm, trading, Composer, Process Manager, Test Tool, Soda Task Manager, and Suwimo service remain disabled.
## 4. Specification and Final Checks
- [x] 4.1 Update the canonical capability Purpose to stop claiming that all six indexes are currently visible.
- [x] 4.2 Compile main and test source sets without running unit tests; automated unit-test coverage is deliberately deferred.
- [x] 4.3 Run strict OpenSpec validation and `git diff --check`.
- [x] 4.4 Review the complete diff for unrelated changes, generic Ticker/Raydium changes, persistence changes, and accidentally enabled services.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-08
@@ -0,0 +1,54 @@
## Context
See `proposal.md` for motivation and the capability delta for the behavioral contract. Issue #64 established that Evelyn owns sampling and complete history while EMC is presentation-only. Ticker already demonstrates durable append with missing-final-newline handling, while configuration parsing demonstrates strict first-actual-entry format declarations. Issue #65 applies those repository patterns to a distinct Evelyn-owned format without changing either existing subsystem.
## Goals / Non-Goals
**Goals:**
- Make a normalized instance name a safe permanent storage identity with atomic JVM-local ownership.
- Make startup transactional from the caller's perspective: validate and parse fully before publishing history or starting a thread.
- Coordinate persistence and publication under Evelyn ownership with durable append ordering.
- Separate restartable stop from irreversible close without releasing a live writer's identity.
**Non-Goals:**
- Creating or repairing runtime storage, cross-process locking, migration, retention, rotation, compaction, additional record types, test creation, or changes to Ticker, Raydium, EMC file access, and unrelated composition.
## Decisions
### Use AutoCloseable for permanent lifecycle completion
`Evelyn` extends `AutoCloseable` and overrides `close()` without a checked exception. `start()` and `stop()` remain the operational lifecycle: stop is restartable and retains the name, while close is permanent and idempotent. This is preferable to an Evelyn-specific disposal name because Java callers can use standard resource ownership and the semantic distinction remains explicit in the contract.
### Validate completely before atomic reservation
Construction first validates every non-name argument and derives the NFC name, case-folded registry key, direct-child directory, and status path. Only after all fallible argument validation succeeds does a static concurrent set reserve the key atomically. The constructor performs no I/O. This prevents invalid construction from leaking reservations and keeps independently constructible components stopped.
### Treat close as release-after-confirmed-termination
Lifecycle state distinguishes stopped, started, stopping, closed, and termination-failed conditions. Stop interrupts and awaits the owned scheduler. Only confirmed termination returns the object to restartable stopped state. Close invokes the same safe stop path and removes the registry key only after confirmation; any failure is reported while the object retains both its non-startable safety state and reservation. Repeated successful close is a no-op.
### Make startup loading transactional
Every start rechecks the directory and file using both metadata/access predicates and actual UTF-8 reading. All lines are read, the dedicated status-format declaration is validated before record parsing, and records are decoded into a temporary list with non-decreasing timestamp checks. Only a successful complete load replaces the in-memory list and establishes the last persisted timestamp. The executor is created last. Failed startup leaves memory empty, storage untouched, and no sampling thread.
### Keep status format validation dedicated
The loader hardcodes `SUPPORTED_STATUS_HISTORY_FORMAT_VERSION = 1` and applies the same declaration convention as configuration files, but produces status-history-specific diagnostics and does not couple version numbers or parsing code to configuration or Ticker formats. Type codes are explicit constants rather than enum ordinals. Full original lines are retained for line diagnostics before comment removal.
### Serialize durable append and publication
The single sampling thread still coordinates the critical operation explicitly. Under the history lock it rejects timestamps earlier than the last persisted timestamp, opens the existing file without create/truncate options, adds a newline first when necessary, writes the encoded UTF-8 record, and forces the channel. Only then does it update the last timestamp and append to visible memory. I/O failures escape this critical operation into the sampling boundary, which logs instance/path/context and leaves scheduling alive.
## Risks / Trade-offs
- [Filesystem access predicates can race with actual operations] → perform the required predicates for useful diagnostics and still treat actual read/open/write/force failures as authoritative.
- [A blocked sampling task may ignore interruption] → never release the name unless executor termination is confirmed; retain the reservation and report failure.
- [JVM-local uniqueness cannot protect another process] → document cross-process locking as out of scope and rely on deployment ownership.
- [Full history loading is unbounded] → accept the issue-defined complete-load behavior; retention and compaction are future work.
- [NFC plus case folding can map spellings to one identity] → preserve the winning normalized display spelling on disk while reserving a `Locale.ROOT` lower-case key.
## Migration Plan
Before enabling each instance, operators create `data/evelyn/Production/status.log` and `data/evelyn/Test/status.log` with physical first line `FORMAT_VERSION=1`. Deployment then starts Ticker, constructs and starts both named Evelyn instances, and opens EMC. Rollback requires stopping/closing the instances but leaves status files untouched; older code will ignore them because it has no Evelyn persistence support.
@@ -0,0 +1,26 @@
## Why
Evelyn currently discards every collected status-index point when it stops, so process restarts lose operational history. Each independently named Evelyn environment needs durable, operator-provisioned append-only storage while Evelyn—not Mission Control—continues to own collection, validation, and publication.
## What Changes
- **BREAKING** Require every `EvelynImpl` to have a validated, NFC-normalized instance name that selects its permanent `data/evelyn/<name>/status.log` identity.
- Reserve normalized instance names atomically and case-insensitively within the JVM until permanent, idempotent close.
- **BREAKING** Extend Evelyn lifecycle with permanent close in addition to restartable start/stop.
- Strictly validate externally provisioned storage, format version 1, every record, and non-decreasing timestamps before publishing restored history or starting sampling.
- Durably append each sampled type-1 status record before publishing it in memory, while isolating individual append failures.
- Keep Production and Test storage, lifecycle, and history independent and keep EMC presentation-only.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `evelyn-status-index-history`: Replace process-local disposable history with named-instance storage, strict restore validation, persist-before-publish behavior, and distinct restartable stop versus permanent close semantics.
## Impact
The change affects the public Evelyn lifecycle, `EvelynImpl` construction and persistence, and temporary Production/Test composition in `NenjimHubImpl`. Operators must provision each instance directory and versioned `status.log` before startup. EMC, AssetAZ Ticker, Raydium PriceSource, generated Detag sources, and unrelated services remain behaviorally unchanged.
@@ -0,0 +1,122 @@
## ADDED Requirements
### Requirement: Evelyn instances have safe unique persistent names
Every Evelyn instance SHALL have an NFC-normalized Unicode name whose preserved spelling and capitalization selects the direct child directory `data/evelyn/<name>`. A name SHALL be rejected if it is empty or whitespace-only, has leading or trailing whitespace, is `.` or `..`, ends with a period, contains a control character or any of `< > : " / \ | ? *`, is a case-insensitive Windows-reserved filename including a reserved base with an extension, or normalizes as a path to anything other than one direct child of `data/evelyn`. Within one JVM, normalized names SHALL be reserved atomically and compared case-insensitively using locale-independent rules.
#### Scenario: A portable Unicode name is accepted
- **WHEN** an instance is constructed with a valid Unicode name containing Danish characters
- **THEN** NFC normalization is applied and the normalized spelling and capitalization identify its direct child data directory
#### Scenario: An unsafe name is rejected without reservation
- **WHEN** construction receives an invalid name or any other invalid constructor argument
- **THEN** construction fails before reserving a persistent identity
#### Scenario: Concurrent equivalent names conflict
- **WHEN** two constructor calls in one JVM concurrently request names that are equal after NFC normalization and locale-independent case folding
- **THEN** exactly one reserves the persistent identity and the other fails
### Requirement: Evelyn distinguishes restartable stop from permanent close
Stopping Evelyn SHALL terminate sampling, clear memory, preserve its persistent file, and retain its name reservation so the same object can restart. Evelyn SHALL support permanent, idempotent close; closing an active instance SHALL first stop it safely, a successfully closed instance SHALL never start again, and its name SHALL be released only after its sampling thread has definitely terminated. A termination failure SHALL be reported and SHALL retain the reservation.
#### Scenario: A stopped instance restarts
- **WHEN** a stopped but not closed Evelyn instance is started again
- **THEN** it retains exclusive ownership of the same name and reloads the complete persisted history before sampling
#### Scenario: An active instance closes successfully
- **WHEN** close is called on an active instance and its sampling thread terminates
- **THEN** memory is cleared, the name reservation is released, and later start calls fail
#### Scenario: Close cannot confirm termination
- **WHEN** close cannot confirm that the sampling thread terminated
- **THEN** close reports failure and retains the name reservation
#### Scenario: Close is repeated
- **WHEN** close is called after the instance was successfully closed
- **THEN** it completes without changing state or failing
### Requirement: Evelyn uses externally provisioned per-instance storage
Each named instance SHALL use the existing regular file `data/evelyn/<normalized-name>/status.log`. Before every start, Evelyn SHALL require the direct instance path to be an existing readable and writable directory and `status.log` to be an existing readable and writable regular file. Evelyn SHALL NOT create, replace, repair, truncate, rename, or otherwise provision either path. Any validation or actual open failure SHALL identify the affected path, leave memory unpublished, and prevent sampling from starting.
#### Scenario: Required storage is absent or unsuitable
- **WHEN** the instance directory or status file is missing, the path has the wrong type, or required access is unavailable
- **THEN** start fails with the affected path and reason without modifying storage, publishing history, or starting sampling
#### Scenario: Storage changes while stopped
- **WHEN** storage becomes invalid after stop and the same instance is started again
- **THEN** the complete startup validation is repeated and restart fails without modifying storage
### Requirement: Evelyn validates status history format strictly
The UTF-8 `status.log` format SHALL have supported version `1`, declared by `FORMAT_VERSION=1` as the first actual entry; blank lines and full-line comments MAY precede it when loading, while an externally provisioned new file SHALL place it on the physical first line. Missing, duplicate, malformed, misplaced, or unsupported declarations SHALL fail loading before record parsing. After the declaration, blank lines and comments SHALL be ignored, inline comments MAY follow data, and every other entry SHALL be a valid record with an explicit stable numeric type code.
Type `1` SHALL encode `<UTC uuuuMMddHHmmssSSS'Z' timestamp>:1:<plain BigDecimal Evelyn Price Index>`. It SHALL have exactly three fields. Unknown types, invalid timestamps or decimals, missing or surplus fields, and decreasing timestamps SHALL fail the complete load; equal timestamps SHALL be accepted. A valid declaration with no records SHALL represent empty history. Applicable failures SHALL identify the file path, one-based line number, and original line, and Evelyn SHALL neither skip, sort, partially publish, nor repair invalid content.
#### Scenario: A valid versioned history loads
- **WHEN** the file has a valid first actual version declaration and valid type-1 records in non-decreasing timestamp order
- **THEN** Evelyn restores every record in file order with zero values for the five unimplemented indexes
#### Scenario: The format declaration is invalid
- **WHEN** the declaration is missing, duplicated, malformed, misplaced, or not version 1
- **THEN** startup fails before parsing records and reports useful file diagnostics
#### Scenario: A record is invalid
- **WHEN** a record has an unknown type, malformed field, wrong field count, or timestamp earlier than its predecessor
- **THEN** startup fails with path, line number, and original line without exposing partial history or altering the file
## MODIFIED Requirements
### Requirement: Evelyn samples without overlapping or catch-up attempts
Each started Evelyn environment SHALL own its sampling executor, attempt its first sample only after complete storage validation and history restoration, and make subsequent attempts approximately one minute after the preceding attempt completes. Sampling SHALL remain active for the Evelyn lifecycle independently of whether Mission Control is open. Evelyn SHALL add exactly one timestamped point per successful attempt and SHALL isolate a failed or temporarily unavailable Ticker lookup or persistence operation without adding a fake or unpersisted point, terminating Evelyn, or cancelling the next attempt. Stopping Evelyn SHALL terminate its sampling executor.
#### Scenario: Initial Ticker data is unavailable
- **WHEN** the immediate sampling attempt cannot obtain a latest EVE/USDT price
- **THEN** no point is persisted or published, diagnostic context is logged, and the next fixed-delay attempt remains scheduled
#### Scenario: A later sample succeeds
- **WHEN** a later attempt obtains a latest EVE/USDT price whose millisecond timestamp is not earlier than the last persisted timestamp
- **THEN** exactly one type-1 record is durably appended before the corresponding point becomes visible in memory
#### Scenario: Persistence fails for one sample
- **WHEN** appending or durably flushing a calculated point fails
- **THEN** the point is not published, instance and path context are logged, and the next fixed-delay attempt remains scheduled
#### Scenario: A sampled timestamp goes backwards
- **WHEN** a newly sampled timestamp is earlier than the last persisted timestamp
- **THEN** no record or point is added, the failure is logged, and later attempts remain scheduled
### Requirement: Evelyn provides historical status-index measurements
Each named Evelyn instance SHALL publish a safe, non-null, oldest-to-newest snapshot only after its complete persisted history has been validated and loaded. Each type-1 point SHALL contain its persisted or sampled timestamp, the Evelyn Price Index, and `BigDecimal.ZERO` placeholders for EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index. Production and Test SHALL use independently named files, history collections, reservations, and lifecycles. A sampled point SHALL be durably appended using robust line-boundary handling before publication. Stop SHALL clear only memory; restart SHALL restore the complete file, and close SHALL preserve the file.
#### Scenario: Status-index history is requested during sampling
- **WHEN** a caller requests an Evelyn environment's status-index history while persisted points may be appended
- **THEN** it receives a safe snapshot containing only completely persisted points and all six typed fields
#### Scenario: A process starts two named Evelyn environments
- **WHEN** Production and Test Evelyn instances start with independently provisioned files
- **THEN** each restores and extends only its own complete history even if calculated values are identical
#### Scenario: Existing file lacks a final line separator
- **WHEN** a valid existing status file ends with a record but no line separator
- **THEN** the next durable append first supplies a line boundary and preserves both records as distinct entries
#### Scenario: An Evelyn instance stops and restarts
- **WHEN** a named instance is stopped and later restarted
- **THEN** its memory is cleared at stop and its complete unchanged persistent history is revalidated and restored before sampling resumes
### Requirement: Evelyn Mission Control visualizes status-index measurements
Evelyn Mission Control SHALL display separate live Production and Test Overview charts exclusively from their corresponding authoritative Evelyn snapshots and SHALL never read or manage persistence directly. Each chart SHALL contain one enabled series named `Evelyn Price Index`; the five future series and their typed fields SHALL remain available as disabled source scaffolding. Opening or reopening EMC SHALL immediately reconstruct each chart from the complete restored current snapshot before periodically refreshing it. Successful points SHALL be added to chart objects only on the JavaFX Application Thread while the window remains open. Timestamp bounds SHALL expand as points arrive, and only enabled Evelyn Price Index values SHALL determine a dynamic Y-axis range that is symmetric around and always displays zero, using a reasonable default range for empty or all-zero history. Hiding or closing EMC SHALL release only EMC's JavaFX refresh resources and SHALL NOT start, stop, close, persist, or clear either Evelyn environment.
#### Scenario: Production and Test Overviews show restored history
- **WHEN** Mission Control opens after two named Evelyn environments have restored their histories
- **THEN** it displays separate one-series charts immediately populated from the corresponding complete snapshots
#### Scenario: A successful point appears while EMC is open
- **WHEN** an Evelyn history publishes a newly persisted point
- **THEN** the corresponding chart adds it on the JavaFX Application Thread and updates its timestamp and symmetric price-index bounds without reopening EMC
#### Scenario: EMC reopens after collecting hidden-window samples
- **WHEN** the same EMC instance is reopened after Evelyn persisted additional points while its window was hidden
- **THEN** each chart is reconstructed immediately from its environment's complete current history without missing points because of stale rendering state
#### Scenario: Future fields contain larger values
- **WHEN** any disabled future-index field has a greater absolute value than Evelyn Price Index
- **THEN** that disabled value does not affect the visible Y-axis bounds
@@ -0,0 +1,23 @@
## 1. Named Identity and Lifecycle
- [x] 1.1 Add NFC portable-name validation, direct-child path derivation, and atomic case-insensitive JVM-local reservation after all constructor validation.
- [x] 1.2 Extend the public Evelyn API with exception-free `AutoCloseable.close()` and implement restartable stop versus permanent idempotent close without releasing a possibly live writer.
- [x] 1.3 Update all required constructors and Production/Test composition to use independent named instances without enabling unrelated services.
## 2. Strict Storage Restoration
- [x] 2.1 Validate the externally provisioned per-instance directory and `status.log` type/access on every start without creating or modifying them.
- [x] 2.2 Validate dedicated status format version 1 as the first actual entry before parsing records, with path and declaration diagnostics.
- [x] 2.3 Strictly decode every type-1 record, reject unknown types and malformed field counts/values, enforce non-decreasing timestamps, and publish only a completely validated temporary history.
## 3. Durable Sampling Persistence
- [x] 3.1 Encode stable type-1 records with UTC millisecond timestamps and plain decimals, preserving blank/comment parsing semantics.
- [x] 3.2 Durably append before in-memory publication, including missing-final-line-separator handling and rejection of backward sampled timestamps.
- [x] 3.3 Isolate append/flush failures with instance, path, and sampling diagnostics so later fixed-delay attempts continue.
## 4. Verification and Review
- [x] 4.1 Compile main and test source sets without running or adding automated tests.
- [x] 4.2 Run strict validation for the active OpenSpec change and `git diff --check`.
- [x] 4.3 Review the complete diff for unrelated changes, behavioral gaps, generated Java edits, runtime data, and changes to Ticker/Raydium specifications or persistence.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-10
@@ -0,0 +1,65 @@
## Context
See `proposal.md` for motivation. Issues #48 through #50 already established `SolanaBlockChain` for RPC-backed account and mint metadata, and `SolanaWallet` for isolated transaction signing. The Swap V2 integration must compose these APIs with Jupiter's `/order` and `/execute` endpoints while retaining module-oriented API and implementation package boundaries.
Jupiter's `/execute` manages transaction landing and can return actual wallet-level totals. Once an execution request has left this process, a transport failure cannot distinguish an unexecuted request from a completed swap whose response was lost.
## Goals / Non-Goals
**Goals:**
- Keep the public API expressed in human-readable ValueTagged amounts while using raw integer units at the Jupiter boundary.
- Validate all locally knowable invariants before wallet signing and execution submission.
- Share the required order pacing across every reference-implementation instance in one JVM.
- Preserve interruption and expose actionable endpoint and response failures through the declared checked exceptions.
**Non-Goals:**
- Exact-output swaps, JupiterZ/RFQ, API-key configuration, retries, direct DEX integration, or a reusable throttling subsystem.
- Constructing an Evelyn burner or changing existing Solana wallet/blockchain responsibilities.
- Adding automated tests in this change; the existing repository verification suites will still be run.
## Decisions
### Compose the established Solana APIs
`JupiterSwapServiceImpl` receives `SolanaBlockChain` and `SolanaWallet`. It obtains the wallet address from the wallet, loads each mint account and supply from the blockchain, and passes Jupiter's Base64 transaction through `SolanaWallet.signTransaction`. Duplicating RPC or signing logic inside the Jupiter implementation was rejected because it would bypass the module contracts established by issues #48 through #50.
### Detect each mint's token program independently
For both input and output, the implementation reads the mint account owner and matches it against `SolanaSPLTokenProgram`. It then requests supply metadata with that program. This supports legacy SPL Token and Token-2022 pairs in any combination and prevents assumptions based on one side of the pair.
### Use exact decimal conversion at the boundary
The requested `BigDecimal` is shifted by the input mint's decimal count and converted with `toBigIntegerExact()`. Actual raw execution totals are parsed as non-negative integers and shifted left with their respective decimal counts. Rounding was rejected because it would silently change the amount authorized by the caller.
### Keep wire types private to the reference implementation
Small private records model only the `/order` and `/execute` fields needed for validation and results. Jackson ignores additional response fields, allowing Jupiter to add metadata without expanding the public API. The stable API package contains only `JupiterSwapService` and `JupiterSwapResult`.
### Use the keyless Swap V2 endpoint and explicitly exclude JupiterZ
The reference implementation calls `https://api.jup.ag/swap/v2/order` and `/execute` without API-key headers. Every order includes `swapMode=ExactIn` and `excludeRouters=jupiterz`; relying on a router default was rejected because JupiterZ/RFQ support is explicitly deferred to issue #66.
### Serialize only order-request starts with a JVM-wide gate
A private static monitor and monotonic timestamp separate request starts by two seconds. Callers wait interruptibly while holding the gate, and the gate remains held for that order exchange so a delayed thread cannot begin out of its reserved sequence. A generalized limiter was rejected as unnecessary scope, and `/execute` bypasses this gate.
### Treat execution submission as a non-retry boundary
The implementation performs one `HttpClient.send` for `/execute` and contains no retry loop around it or the whole swap. Any failure after submission propagates, with a message warning that execution may be unknown and balances must be reloaded. Retrying was rejected because it can duplicate a financially consequential action.
### Validate response integrity in phases
Mint and amount checks occur before `/order`; order identity, taker, slippage, Base64 transaction content, and transaction metadata checks occur before signing; execution status and explicit result code, signature, and actual totals are checked before constructing the public result. Numeric wire fields that must distinguish an absent value from zero use nullable DTO types. Non-2xx responses retain status and body context, while malformed JSON is wrapped as `IOException`.
## Risks / Trade-offs
- [Jupiter may change its wire schema or endpoint behavior] → Decode a minimal tolerant DTO set, but strictly validate every field used for signing and result construction.
- [A submitted execution can succeed despite a local timeout or interruption] → Never retry and report the outcome as unknown so callers reload balances before deciding what to do.
- [A slow order exchange serializes later order callers] → This intentionally small implementation-specific gate prioritizes strict JVM-wide start spacing and remains completely separate from execution requests.
- [Keyless service availability or limits can change] → Surface HTTP response status/body clearly; API-key configuration remains intentionally out of scope.
## Migration Plan
This is an additive API and implementation. Downstream composition can instantiate the reference implementation with its existing blockchain and wallet instances. Rollback consists of removing the new package because no existing API or persisted data is changed.
@@ -0,0 +1,26 @@
## Why
Nenjim needs a reusable service that can exchange an exact, human-readable SPL-token amount through Jupiter while preserving the repository's existing Solana wallet and blockchain boundaries. Jupiter Swap V2 supplies the quote, transaction construction, and managed execution required for this flow.
## What Changes
- Add a public `JupiterSwapService` API and immutable `JupiterSwapResult` value describing the confirmed transaction and actual amounts spent and received.
- Add a reference implementation for exact-input Jupiter Swap V2 `/order`, wallet signing, and `/execute` processing.
- Resolve both mint programs and decimal precision through `SolanaBlockChain`, supporting the legacy SPL Token Program and Token-2022.
- Validate caller input, mint metadata, order integrity, transaction metadata, and execution results before returning success.
- Exclude the `jupiterz` router from every order while JupiterZ/RFQ support remains deferred.
- Enforce a small JVM-wide two-second minimum interval between `/order` requests, without delaying `/execute` or automatically retrying submitted executions.
## Capabilities
### New Capabilities
- `jupiter-swap-service`: Exact-input SPL-token swaps through Jupiter Swap V2, including validation, signing, managed execution, throttling, and failure semantics.
### Modified Capabilities
None.
## Impact
This adds public API types under `com.r35157.libs.jupiter.swap` and a reference implementation under `com.r35157.libs.jupiter.swap.impl.ref`. It depends on the existing `SolanaBlockChain` and `SolanaWallet` APIs and Jupiter's keyless Swap V2 HTTP endpoints; it does not add API-key configuration, Evelyn burning, RFQ/JupiterZ support, or a general throttling framework.
@@ -0,0 +1,94 @@
## Purpose
Defines safe exact-input SPL-token swaps through Jupiter Swap V2 using human-readable amounts and an existing Solana wallet.
## ADDED Requirements
### Requirement: Public exact-input swap contract
The service SHALL accept distinct input and output SPL mint addresses, a positive human-readable input amount, and a maximum slippage in basis points. It SHALL return the confirmed Solana transaction signature together with the actual human-readable input amount spent and output amount received.
#### Scenario: Successful exact-input swap
- **WHEN** a caller requests a valid exact-input swap that Jupiter executes successfully
- **THEN** the result contains the confirmed signature and the actual spent and received amounts converted with their respective mint decimal precision
#### Scenario: Invalid caller input
- **WHEN** either mint is blank, both mints are equal, the amount is null or non-positive, or the maximum slippage is outside 0 through 10000 basis points
- **THEN** the service rejects the request before requesting a Jupiter order
### Requirement: On-chain mint validation and exact amount conversion
Before requesting an order, the service SHALL load both mint accounts through the Solana blockchain, require each mint to exist and be owned by either the legacy SPL Token Program or Token-2022, and resolve each mint's decimal precision. The service SHALL convert the input amount exactly to a positive raw integer and SHALL reject values that cannot be represented with the input mint's precision.
#### Scenario: Supported legacy and Token-2022 mints
- **WHEN** the input and output mint accounts are owned by either supported SPL token program and their supplies provide valid decimal precision
- **THEN** the service uses those independently resolved precisions for raw request and human-readable result amounts
#### Scenario: Missing or unsupported mint
- **WHEN** either mint account is absent or is owned by an unsupported program
- **THEN** the service fails before requesting an order or signing a transaction
#### Scenario: Fraction smaller than the mint unit
- **WHEN** the requested input amount has a non-zero fraction beyond the input mint's decimal precision
- **THEN** the service rejects the amount rather than rounding it
### Requirement: Safe Jupiter order acquisition
The service SHALL use Jupiter's keyless Swap V2 order endpoint in `ExactIn` mode with the wallet as taker, the caller's slippage limit, and `excludeRouters=jupiterz`. Across all reference-implementation instances in one JVM, starts of order HTTP requests SHALL be separated by at least two seconds. This limiter SHALL remain local to the implementation and SHALL NOT delay execution requests.
#### Scenario: Every order excludes JupiterZ
- **WHEN** the service requests an order
- **THEN** the request identifies the exact input and output mints, raw input amount, wallet taker, exact-input mode, slippage limit, and excludes the `jupiterz` router
#### Scenario: Concurrent service instances request orders
- **WHEN** multiple reference-implementation instances concurrently need Jupiter orders
- **THEN** their order HTTP requests begin at least two seconds apart JVM-wide while execution requests remain unthrottled
#### Scenario: Order wait is interrupted
- **WHEN** a caller is interrupted while waiting for its permitted order-request time
- **THEN** the service propagates interruption without sending that order request
### Requirement: Order integrity validation before signing
Before signing, the service SHALL require a successful HTTP response containing a well-formed order whose input mint, output mint, raw input amount, exact-input mode, and taker match the request. The returned slippage SHALL be present, valid, and no greater than the caller's maximum. The service SHALL also require a non-blank, valid Base64 unsigned transaction that decodes to at least one byte, a non-blank request identifier, and a positive valid last block height. Any order build error, malformed body, mismatch, or missing required value SHALL fail before signing.
#### Scenario: Valid matching order
- **WHEN** Jupiter returns a well-formed order matching both requested mints, the exact raw input amount, wallet taker, and permitted slippage with all required transaction metadata
- **THEN** the service passes the returned unsigned transaction to the configured Solana wallet for signing
#### Scenario: Mismatching or incomplete order
- **WHEN** an order changes a mint, amount, swap mode, or taker; returns missing, invalid, or excessive slippage; or lacks a valid non-empty Base64 transaction, request identifier, or last valid block height
- **THEN** the service rejects the order without signing or executing it
#### Scenario: Jupiter order HTTP or decoding failure
- **WHEN** the order endpoint returns a non-success status or malformed JSON
- **THEN** the service reports an I/O failure with useful endpoint response context and does not sign a transaction
### Requirement: Single-attempt managed execution
After successful signing, the service SHALL reject a blank signed transaction and submit exactly one Swap V2 execution request containing the signed transaction, order request identifier, and last valid block height. The service SHALL never automatically retry after the execution request has been submitted, because transport failure, timeout, or interruption can leave the execution outcome unknown.
#### Scenario: Signed transaction is executed once
- **WHEN** wallet signing returns a non-blank transaction
- **THEN** the service sends one execution request without applying the order limiter
#### Scenario: Execution outcome is unknown
- **WHEN** the execution HTTP exchange times out, is interrupted, loses its response, or otherwise fails after submission
- **THEN** the service reports the failure and does not automatically request another order or resubmit execution
#### Scenario: Blank signed transaction
- **WHEN** wallet signing returns a blank serialized transaction
- **THEN** the service fails without submitting an execution request
### Requirement: Confirmed execution result validation
The service SHALL return success only when the execution response has status `Success`, explicitly has result code `0`, contains a non-blank transaction signature, and contains positive valid actual total input and output raw amounts. It SHALL convert each actual amount using the independently resolved precision of its mint. Non-success HTTP responses, malformed responses, expired or rejected swaps, failed or contradictory status/code combinations, missing result codes, blank signatures, and invalid result amounts SHALL be reported as failures.
#### Scenario: Successful execution response
- **WHEN** Jupiter reports status `Success`, code `0`, a signature, and valid actual total input and output amounts
- **THEN** the service returns those actual amounts in human-readable units and the reported transaction signature
#### Scenario: Jupiter rejects or fails execution
- **WHEN** Jupiter returns a failed status, a missing or non-zero result code, a contradictory status/code combination, expiration, rejection, non-success HTTP status, malformed body, blank signature, or invalid actual amount
- **THEN** the service reports an I/O failure and does not represent the swap as successful
### Requirement: API and implementation separation
The stable service interface and result value SHALL reside in the Jupiter swap API package, while HTTP DTOs, endpoint handling, throttling, and the reference implementation SHALL remain in the reference-implementation package.
#### Scenario: Downstream API consumer
- **WHEN** downstream code depends only on the Jupiter swap API package
- **THEN** it can invoke swaps and inspect results without depending on private wire DTOs or reference-implementation mechanics
@@ -0,0 +1,25 @@
## 1. Public API
- [x] 1.1 Add the ValueTagged `JupiterSwapService` exact-input method with complete public Javadoc and validation/failure contracts.
- [x] 1.2 Add the immutable `JupiterSwapResult` value containing the signature and actual human-readable amounts.
## 2. Reference Implementation
- [x] 2.1 Add the reference implementation with injected `SolanaBlockChain` and `SolanaWallet` dependencies and private Swap V2 wire DTOs.
- [x] 2.2 Validate arguments, both on-chain mint accounts and programs, mint decimal metadata, and exact raw input conversion before order acquisition.
- [x] 2.3 Implement keyless `/order` acquisition in exact-input mode, always excluding JupiterZ, with JVM-wide interruptible two-second request-start pacing.
- [x] 2.4 Validate order identity and transaction metadata before signing through `SolanaWallet`.
- [x] 2.5 Submit a signed transaction once through `/execute`, without retries or order throttling, and validate the confirmed result and actual amounts.
## 3. Verification
- [x] 3.1 Compile Detag-generated main and test source sets and run the repository's existing test/check tasks without adding automated tests.
- [x] 3.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 3.3 Review the complete diff for stale imports, generated-file edits, unrelated refactors, tests, and any implementation that violates the module or retry boundaries.
## 4. Review Follow-up Validation
- [x] 4.1 Validate the returned taker and nullable slippage before signing.
- [x] 4.2 Base64-decode the returned transaction and reject invalid or empty decoded content before signing.
- [x] 4.3 Require both execution status `Success` and an explicit result code `0`.
- [x] 4.4 Re-run compilation, Detag, existing tests, strict OpenSpec validation, and diff checks without adding tests.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,62 @@
## Context
See `proposal.md` for motivation. `SolanaBlockChainImpl` currently serializes every RPC request through a synchronized `sendThrottled()` method, sleeps until five seconds have elapsed since the preceding RPC response, performs HTTP while holding the monitor, and records completion time in `finally`. Monitor acquisition is neither interruptible nor deadline-aware, while issue #67 requires both without changing existing RPC behavior.
## Goals / Non-Goals
**Goals:**
- Preserve one shared five-second gate for all RPC calls while allowing awaiting calls to stop at a monotonic deadline.
- Keep definitive transaction state, timeout, protocol failure, and interruption as distinct outcomes.
- Strictly validate all wire data that controls a returned outcome.
- Keep the API and outcome types generic to Solana rather than any consuming domain.
**Non-Goals:**
- Combining submission and awaiting, resubmission, WebSockets, signature subscriptions, or blockhash-expiration inference.
- Updating any existing wallet, Jupiter, burn, Evelyn, or Discord consumer.
- Adding automated tests or test-only transport, time, or throttling seams in this implementation.
## Decisions
### Replace the intrinsic monitor with one interruptible shared lock
A private `ReentrantLock` will serialize all RPC traffic. Existing calls acquire it interruptibly, perform the same five-second wait and HTTP exchange, update the shared completion timestamp in `finally`, and release it. Awaiting calls use timed `tryLock` with their remaining deadline. A separate polling lock or rate limiter was rejected because it would violate the shared throttle and permit polling to interfere unpredictably with other calls.
### Measure shared throttling and awaiting deadlines monotonically
The completion timestamp and deadline calculations use `System.nanoTime()`. A small private deadline value tracks the call's start and saturated timeout nanoseconds and computes remaining time by elapsed duration. Wall-clock time was rejected because clock adjustments could extend or shorten both throttling and the caller's timeout.
### Build each status HTTP request only after gate and throttle admission
The deadline-aware send path acquires the gate and waits only up to the remaining deadline. Once admitted, it creates a request whose `HttpRequest.timeout` equals the then-current remaining duration. A request-timeout exception is translated to `TIMED_OUT`; other I/O failures remain `IOException`. Building the request after admission avoids assigning the full original timeout to an HTTP exchange that starts after gate/throttle delay.
### Return an internal no-request signal on deadline expiry
The deadline-aware send helper returns no response when the deadline expires before request start. The public loop converts this only to the invariant `TIMED_OUT` value. This keeps deadline expiry separate from communication/protocol exceptions and ensures a previously seen lower-commitment slot cannot leak into the timeout result.
### Parse only authoritative signature-status fields
Each response must contain a successful HTTP result, no JSON-RPC error, and exactly one `result.value` element. A null element remains inconclusive. A status object requires a Long-representable non-negative `slot` and recognized `confirmationStatus`; only after its commitment satisfies the request is `err` interpreted as success or failure. `result.context`, `confirmations`, and legacy `status` are ignored.
### Preserve the complete on-chain error as compact JSON
Jackson's compact serialization of the non-null `err` node becomes `failureDetails`. The blockchain layer does not classify program-specific errors. Any issue outside this `err` node remains an exception or timeout rather than `FAILED`.
### Validate transaction signatures with the existing Base58 implementation
The reference implementation reuses its existing private Base58 decoder and requires exactly 64 decoded bytes before entering the RPC gate. Introducing a second codec dependency or general transaction parser was rejected as unrelated scope.
### Make cached awaiting a transparent pass-through
`CachedSolanaBlockChain.awaitTransaction` calls its delegate directly. Transaction status is deadline-sensitive and mutable, so cache keys, reuse, and in-flight deduplication would violate the contract.
## Risks / Trade-offs
- [An HTTP client can complete at the edge of its timeout after the monotonic deadline] → Use the remaining deadline as the request timeout and re-check the deadline before interpreting an inconclusive response; this is the strongest practical bound offered by Java `HttpClient`.
- [Replacing synchronization changes the internal locking primitive] → Keep the same lock scope, completion-based five-second spacing, and exception behavior for every existing RPC operation.
- [No automated deterministic coverage is added now] → Keep deadline, parsing, and gate concerns in narrow private methods and document that focused tests remain deferred by explicit task instruction.
## Migration Plan
This is additive at the public API. Existing implementations in this repository are updated together, while existing consumers remain unchanged. Rollback removes the new method/types and restores the prior private monitor without data migration.
@@ -0,0 +1,26 @@
## Why
Submitting a Solana transaction only proves RPC acceptance, leaving callers unable to distinguish a requested on-chain commitment outcome from an unknown pending or dropped transaction. A generic wait operation is needed so future consumers can explicitly await processed, confirmed, or finalized outcomes without coupling submission to confirmation.
## What Changes
- Add public Solana commitment and transaction-outcome types with strict status invariants and comprehensive timeout semantics.
- Add `SolanaBlockChain.awaitTransaction(...)` to poll `getSignatureStatuses` for an already submitted signature.
- Reuse the existing shared five-second RPC throttle while making gate acquisition interruptible and bounded by an overall monotonic deadline.
- Validate signatures and timeouts locally, strictly validate RPC responses, and reserve `FAILED` exclusively for on-chain execution errors observed at the requested commitment.
- Delegate every cached-wrapper call directly without caching or deduplication.
- Add the `SolanaSlot` ValueTag backed by nullable `Long`.
## Capabilities
### New Capabilities
- `solana-transaction-awaiting`: Generic blocking observation of an existing Solana transaction through a requested commitment level, with definitive success/failure and unknown-timeout semantics.
### Modified Capabilities
None.
## Impact
The Solana public API, reference RPC implementation, cached decorator, and shared Detag configuration are extended. Existing RPC operations retain their public behavior and common throttling; no wallet, Jupiter, burn, Evelyn, WebSocket, or transaction-submission consumer is changed to wait automatically.
@@ -0,0 +1,96 @@
## Purpose
Defines how callers await a definitive Solana transaction outcome at an explicitly requested commitment without resubmitting the transaction.
## ADDED Requirements
### Requirement: Separate transaction awaiting API
The Solana blockchain API SHALL expose a blocking operation that accepts an existing 64-byte Base58 transaction signature, a requested commitment of `PROCESSED`, `CONFIRMED`, or `FINALIZED`, and a strictly positive overall timeout. Submission and awaiting SHALL remain separate operations, and existing consumers SHALL NOT begin awaiting automatically.
#### Scenario: Valid await request
- **WHEN** a caller supplies a valid signature, non-null commitment, and positive timeout
- **THEN** the service observes that already submitted transaction without submitting or resubmitting it
#### Scenario: Invalid arguments
- **WHEN** the signature is null, blank, invalid Base58, or does not decode to exactly 64 bytes, the commitment is null, or the timeout is null, zero, or negative
- **THEN** the service rejects the call locally before making an RPC request
### Requirement: Commitment-aware definitive outcomes
The commitment order SHALL be `PROCESSED < CONFIRMED < FINALIZED`, and an observed level SHALL satisfy the same or any lower requested level. The service SHALL return `SUCCEEDED` or `FAILED` only after the observed transaction has reached or exceeded the requested commitment. `FAILED` SHALL exclusively represent a non-null Solana on-chain `err`; lower-commitment success or error observations SHALL remain inconclusive.
#### Scenario: Success reaches requested commitment
- **WHEN** the transaction is observed at or above the requested commitment with `err: null`
- **THEN** the service returns `SUCCEEDED` with the transaction slot and no failure details
#### Scenario: Failure reaches requested commitment
- **WHEN** the transaction is observed at or above the requested commitment with a non-null `err`
- **THEN** the service returns `FAILED` with the transaction slot and the complete `err` value as compact JSON
#### Scenario: Observation is below requested commitment
- **WHEN** a successful or failed transaction is observed below the requested commitment
- **THEN** the service continues polling rather than returning a definitive outcome
### Requirement: Transaction outcome invariants
Every public transaction outcome SHALL enforce that `SUCCEEDED` has a non-null slot and null failure details, `FAILED` has a non-null slot and non-blank failure details, and `TIMED_OUT` has null slot and null failure details.
#### Scenario: Invalid outcome construction
- **WHEN** an outcome is constructed with fields inconsistent with its status
- **THEN** construction fails immediately
### Requirement: Signature status polling contract
The service SHALL poll HTTP JSON-RPC `getSignatureStatuses` with exactly the supplied signature and `searchTransactionHistory: true`. It SHALL use only `result.value[0].slot`, `err`, and `confirmationStatus`; a null sole value SHALL remain inconclusive. A present status SHALL contain a Long-representable slot and one of `processed`, `confirmed`, or `finalized`.
#### Scenario: Signature is not found yet
- **WHEN** `result.value` contains exactly one null element
- **THEN** the service continues polling until a definitive outcome or timeout
#### Scenario: Valid status is observed
- **WHEN** the sole status contains a valid slot, recognized confirmation status, and `err` value
- **THEN** the service evaluates it against the requested commitment
### Requirement: Overall monotonic deadline
The timeout SHALL define one monotonic deadline covering RPC-gate acquisition, shared throttling, HTTP calls, response processing, and subsequent polls. Gate and throttle waiting SHALL be interruptible and deadline-aware, no request SHALL start after the deadline, and an HTTP request SHALL as far as reasonably possible be bounded by the remaining time.
#### Scenario: Deadline expires before RPC access
- **WHEN** the deadline expires while waiting for the shared gate or remaining throttle interval
- **THEN** the service returns `TIMED_OUT` without starting another RPC request
#### Scenario: Deadline expires during HTTP request
- **WHEN** the deadline-bound status HTTP request times out
- **THEN** the service returns `TIMED_OUT`
#### Scenario: Caller is interrupted
- **WHEN** interruption occurs during gate acquisition, throttling, HTTP communication, or another internal wait
- **THEN** `InterruptedException` propagates directly and no subsequent request is made
### Requirement: Unknown timeout semantics
`TIMED_OUT` SHALL mean the requested definitive outcome remains unknown and SHALL NOT imply rejection, failure, expiration, or permission to resubmit. It SHALL not retain a slot previously observed below the requested commitment.
#### Scenario: Deadline passes without definitive outcome
- **WHEN** the signature remains unseen or below the requested commitment until the deadline
- **THEN** the service returns `TIMED_OUT` with null slot and null failure details and does not resubmit the transaction
### Requirement: Shared RPC throttling
Status polling SHALL reuse the same shared mechanism that preserves at least five seconds between Solana RPC calls. It SHALL add no independent polling sleep, interval, or limiter, and existing RPC operations SHALL preserve their public behavior and common throttling.
#### Scenario: Inconclusive poll
- **WHEN** a status response is inconclusive and time remains
- **THEN** the next poll is naturally delayed by the shared five-second RPC throttle only
### Requirement: Communication and protocol errors
Non-success HTTP responses, JSON-RPC errors, invalid JSON, missing or invalid `result.value`, arrays with other than exactly one element, invalid slots, missing or unknown confirmation status, and other network or protocol failures SHALL result in `IOException`, not `FAILED`. Error messages SHALL identify `getSignatureStatuses` and include relevant context. The service SHALL NOT automatically retry after an `IOException`.
#### Scenario: Invalid RPC response
- **WHEN** a status response violates the required HTTP, JSON-RPC, result-array, slot, or confirmation-status contract
- **THEN** the service throws a contextual `IOException`
#### Scenario: Communication failure
- **WHEN** status communication fails for a reason other than expiration of the overall deadline or thread interruption
- **THEN** the service throws `IOException` without automatically polling again
### Requirement: Cached decorator delegates directly
The cached Solana blockchain decorator SHALL delegate every transaction-await call directly to its underlying blockchain without caching, reuse, or deduplication.
#### Scenario: Repeated awaits through cached decorator
- **WHEN** callers invoke transaction awaiting multiple times through the cached decorator
- **THEN** every invocation reaches the delegate independently
@@ -0,0 +1,24 @@
## 1. Public Solana API
- [x] 1.1 Add the nullable-Long `SolanaSlot` ValueTag through the shared Detag configuration.
- [x] 1.2 Add documented `SolanaCommitment` and invariant-enforcing `SolanaTransactionOutcome` public types.
- [x] 1.3 Add the documented `awaitTransaction(...)` contract to `SolanaBlockChain`.
## 2. RPC Gate and Polling
- [x] 2.1 Replace intrinsic RPC synchronization with one interruptible shared gate while preserving completion-based five-second throttling for existing operations.
- [x] 2.2 Add local signature, commitment, and timeout validation plus a monotonic overall deadline.
- [x] 2.3 Implement deadline-aware `getSignatureStatuses` polling with `searchTransactionHistory: true` and no independent polling delay.
- [x] 2.4 Strictly validate HTTP, JSON-RPC, result-array, slot, confirmation-status, and error payload semantics.
- [x] 2.5 Return definitive outcomes only at the requested commitment and preserve direct interruption, unknown timeout, and no-retry behavior.
## 3. Cached Decorator and Scope
- [x] 3.1 Delegate cached-wrapper awaits directly without caching, reuse, or deduplication.
- [x] 3.2 Confirm no existing wallet, Jupiter, burn, Evelyn, Discord, or submission flow adopts automatic awaiting.
## 4. Verification
- [x] 4.1 Compile main and test source sets with normal Detag generation and run existing relevant verification without adding tests.
- [x] 4.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 4.3 Review the complete diff for protocol gaps, deadline violations, generated-file edits, new tests, and unrelated changes.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,64 @@
## Context
Issue #67 introduced a generic, blocking `SolanaBlockChain.awaitTransaction()` operation with explicit commitment, timeout, and outcome semantics. Jupiter Swap and Perps already own their respective build, validation, signing, and single provider-submission flows, so they are the correct boundaries for adding independent Solana confirmation. Their public APIs and implementations are already package-separated even though they currently share this repository.
## Goals / Non-Goals
**Goals:**
- Add one consistent Jupiter-level representation for definitive on-chain failure and unknown timeout.
- Keep provider-response validation visibly separate from the subsequent independent Solana confirmation.
- Preserve existing constructors and operation return types while making confirmation timeout an injectable high-level policy.
- Keep increase and decrease confirmation behavior identical and keep alarm reporting outcome-aware.
**Non-Goals:**
- Changing blockchain awaiting, wallet submission, transaction construction, signing, Jupiter provider submission, or financial validation.
- Adding retries, resubmission, persistent reconciliation, configuration changes, or automated tests.
- Integrating confirmation into burn, Evelyn, Discord, or other consumers.
## Decisions
### Put the outcome exception in the common Jupiter API package
`JupiterTransactionOutcomeException` will be a checked public exception in `com.r35157.libs.jupiter`, shared by Swap and Perps without making either API depend on the other. It will defensively validate and retain the signature, requested commitment, and complete `SolanaTransactionOutcome`. Its message will derive definitive-failure or unknown-timeout diagnostics from that structured state.
An exception local to each service was rejected because it would duplicate identical cross-provider outcome semantics and prevent callers such as alarm actions from applying one policy.
### Confirm only after complete provider validation
Swap will build its `JupiterSwapResult` only after all existing `/execute` status, code, signature, and actual-amount checks pass, then await the result signature. Perps will await only after its existing execution response yields a valid non-blank `txid`. This maintains two explicit trust boundaries: Jupiter validates its response first; Solana independently establishes the on-chain outcome second.
Treating provider success as implicit confirmation was rejected because it is the gap this change closes. Replacing provider validation with Solana confirmation was rejected because on-chain status cannot validate Jupiter's reported token amounts or response contract.
### Keep timeout policy immutable in each service instance
Each implementation will store a validated `Duration`. Existing constructors delegate to overloads using a two-minute constant. The duration is passed unchanged to each `awaitTransaction()` call at `CONFIRMED`, so pre-confirmation work does not consume it.
A configuration-file setting was rejected because issue #68 deliberately defines constructor policy only. A timeout in wallet or blockchain submission was rejected because confirmation belongs to the high-level operation.
### Centralize confirmation policy at the smallest useful scope
Perps increase and decrease will call one private helper that invokes `awaitTransaction()` and switches on all three outcomes. Swap will use its own equivalent private helper because the implementations are separate future artifacts; sharing implementation mechanics across them would create an unnecessary coupling.
Both helpers wrap only confirmation `IOException`s with operation, signature, and unknown-outcome context, preserve the cause, allow `InterruptedException` to propagate directly, and never retry. This avoids accidentally changing existing pre-signature/provider error behavior.
### Keep alarms presentation-only for confirmation results
The two alarm actions will catch `JupiterTransactionOutcomeException` before their generic error handling and format `FAILED` and `TIMED_OUT` separately. They will not call the blockchain API or attempt reconciliation, preserving the Jupiter service as owner of complete operation orchestration.
## Risks / Trade-offs
- [A provider submission can succeed while confirmation communication fails] → Preserve the known signature, report the on-chain outcome as unknown, retain the cause, and prohibit automatic resubmission.
- [Synchronous confirmation extends operation latency] → Use an immutable configurable timeout with a safe two-minute default and preserve interruption.
- [Alarm reporting is transient and not reconciled] → Clearly label timeout as unknown; persistent reconciliation remains explicitly deferred.
- [Checked exception propagation affects callers at compile time] → Update both public service contracts and every affected call site in this repository consistently.
## Migration Plan
1. Add the common checked exception and update both public service contracts.
2. Add compatible implementation constructor overloads and confirmation helpers.
3. Update alarm actions for structured outcome reporting.
4. Compile main and test source sets, run available non-live checks, and retain the active OpenSpec change for review.
Rollback consists of reverting this change as one unit; no data or configuration migration is involved.
@@ -0,0 +1,27 @@
## Why
Jupiter's provider responses currently end Swap and Perps operations without independently confirming the returned transaction signature on Solana. The shared `SolanaBlockChain.awaitTransaction()` API now makes it possible for these high-level operations to report success only after `CONFIRMED` on-chain success while preserving definitive failure and unknown-timeout outcomes.
## What Changes
- Add a shared checked Jupiter outcome exception that preserves the submitted signature, requested commitment, and complete failed or timed-out Solana outcome.
- Make Jupiter Swap independently await its validated execution signature at `CONFIRMED` before returning its existing result.
- Make Jupiter Perps increase and decrease independently await their valid execution signatures at `CONFIRMED` through one common private policy helper.
- Add validated constructor-injected confirmation timeouts while preserving existing constructors with a two-minute default.
- Preserve Jupiter provider-response validation as a distinct prerequisite to Solana confirmation, including Swap actual-amount validation.
- Add outcome-specific Perps alarm reporting without moving confirmation into alarm actions or introducing retries or resubmission.
- Update affected public Javadoc and checked-exception declarations.
## Capabilities
### New Capabilities
- `jupiter-perps-transaction-confirmation`: Defines independent on-chain confirmation and alarm reporting for Jupiter Perps increase and decrease operations.
### Modified Capabilities
- `jupiter-swap-service`: Requires independent `CONFIRMED` Solana success after the existing Jupiter execution-response validation and defines structured failure and timeout handling.
## Impact
The Jupiter common API gains one checked exception. The Jupiter Swap and Perps APIs, their reference implementations, and the two Perps alarm actions gain corresponding confirmation and error propagation behavior. Existing transaction construction, signing, provider submission, wallet methods, pacing, and retry behavior remain unchanged.
@@ -0,0 +1,68 @@
## Purpose
Defines independent Solana confirmation and outcome reporting for completed Jupiter Perps increase and decrease submissions.
## ADDED Requirements
### Requirement: Confirmed Perps operation completion
After the existing Perps increase or decrease flow produces a valid non-blank transaction signature, the service SHALL independently await that signature at `CONFIRMED`. It SHALL return the existing signature result only for `SUCCEEDED`; provider response validation and financial validation SHALL remain distinct prerequisites and SHALL not themselves constitute on-chain confirmation.
#### Scenario: Increase reaches confirmed success
- **WHEN** an increase submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
- **THEN** the service returns that signature
#### Scenario: Decrease reaches confirmed success
- **WHEN** a decrease submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
- **THEN** the service returns that signature
### Requirement: Structured exceptional outcomes
The shared Jupiter API SHALL expose a checked outcome exception containing the known non-blank transaction signature, requested commitment, and complete transaction outcome. Construction SHALL reject null or blank signatures, null commitments, null outcomes, and `SUCCEEDED` outcomes. `FAILED` SHALL identify a definitive on-chain failure including its slot and compact Solana error, while `TIMED_OUT` SHALL identify an unknown outcome and explicitly warn against automatic equivalent resubmission.
#### Scenario: Confirmed on-chain failure
- **WHEN** an increase or decrease signature reaches `CONFIRMED` with a `FAILED` outcome
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, slot, and complete compact Solana failure details
#### Scenario: Confirmation timeout
- **WHEN** an increase or decrease signature produces a `TIMED_OUT` outcome
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, and unknown outcome without automatically resubmitting
#### Scenario: Successful outcome is supplied to exception
- **WHEN** a caller attempts to construct the outcome exception with `SUCCEEDED` or inconsistent null or blank data
- **THEN** construction fails immediately
### Requirement: Perps confirmation timeout policy
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be forwarded unchanged and SHALL begin governing only when awaiting the already submitted signature starts. Increase and decrease SHALL apply the same confirmation and outcome policy.
#### Scenario: Existing constructor is used
- **WHEN** the reference service is constructed without a confirmation timeout
- **THEN** each submitted increase or decrease uses a two-minute confirmation timeout
#### Scenario: Custom timeout is used
- **WHEN** the reference service is constructed with a positive duration
- **THEN** each submitted increase or decrease forwards exactly that duration to `CONFIRMED` awaiting
#### Scenario: Invalid timeout is supplied
- **WHEN** the reference service is constructed with a null, zero, or negative duration
- **THEN** construction fails before any Perps operation can be submitted
### Requirement: Post-submission communication and interruption policy
If independent confirmation throws an I/O error after a signature is known, the service SHALL throw an `IOException` whose message identifies the increase or decrease operation, includes the signature, and states that the on-chain outcome is unknown, retaining the original error as cause. `InterruptedException` SHALL propagate directly without another RPC or Jupiter request. Confirmation SHALL never submit, rebuild, re-sign, retry, or resubmit a transaction.
#### Scenario: Confirmation communication fails
- **WHEN** independent confirmation throws an I/O error for a known Perps signature
- **THEN** the service reports the operation, signature, and unknown outcome, preserves the original error, and performs no retry or resubmission
#### Scenario: Confirmation is interrupted
- **WHEN** independent confirmation throws `InterruptedException`
- **THEN** interruption propagates directly and no subsequent request occurs
### Requirement: Outcome-aware Perps alarm reporting
Perps increase and decrease alarm actions SHALL rely on the Perps service for confirmation and SHALL distinguish structured `FAILED`, structured `TIMED_OUT`, and other errors. A failed report SHALL include operation, signature, confirmation slot, and compact Solana failure details. A timed-out report SHALL include operation, signature, unknown-outcome status, and that no equivalent transaction was automatically resubmitted. Alarm actions SHALL NOT invoke transaction awaiting themselves.
#### Scenario: Alarm operation fails on-chain
- **WHEN** a Perps alarm action receives a structured `FAILED` outcome exception
- **THEN** it reports the operation, signature, confirmation slot, and compact on-chain failure details distinctly from generic errors
#### Scenario: Alarm operation remains unknown
- **WHEN** a Perps alarm action receives a structured `TIMED_OUT` outcome exception
- **THEN** it reports the operation, signature, unknown outcome, and absence of automatic equivalent resubmission distinctly from generic errors
@@ -0,0 +1,44 @@
## MODIFIED Requirements
### Requirement: Confirmed execution result validation
The service SHALL first accept a Jupiter execution response only when it has status `Success`, explicitly has result code `0`, contains a non-blank transaction signature, and contains positive valid actual total input and output raw amounts. It SHALL convert each actual amount using the independently resolved precision of its mint. After this provider-response validation, the service SHALL independently await the returned signature on Solana at `CONFIRMED` using its configured confirmation timeout and SHALL return the already validated swap result only for a `SUCCEEDED` outcome. Non-success HTTP responses, malformed responses, expired or rejected swaps, failed or contradictory status/code combinations, missing result codes, blank signatures, and invalid result amounts SHALL remain provider failures and SHALL not be represented as successful swaps.
#### Scenario: Provider response and on-chain confirmation succeed
- **WHEN** Jupiter reports status `Success`, code `0`, a signature, and valid actual total input and output amounts, and that signature independently reaches `CONFIRMED` with a `SUCCEEDED` outcome
- **THEN** the service returns the signature and those actual amounts in human-readable units
#### Scenario: Jupiter rejects or fails execution
- **WHEN** Jupiter returns a failed status, a missing or non-zero result code, a contradictory status/code combination, expiration, rejection, non-success HTTP status, malformed body, blank signature, or invalid actual amount
- **THEN** the service reports an I/O failure without treating provider validation as an on-chain confirmation
#### Scenario: Provider succeeds but transaction fails on-chain
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `FAILED`
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, slot, and complete compact Solana failure details
#### Scenario: Provider succeeds but confirmation times out
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `TIMED_OUT`
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, and unknown outcome warning and does not automatically resubmit an equivalent transaction
## ADDED Requirements
### Requirement: Swap confirmation timeout policy
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be passed unchanged to independent confirmation after submission and SHALL begin governing only when awaiting the known signature starts.
#### Scenario: Valid custom timeout
- **WHEN** a reference service is constructed with a positive duration and later obtains a validated execution signature
- **THEN** it awaits that signature at `CONFIRMED` with exactly the supplied duration
#### Scenario: Invalid custom timeout
- **WHEN** a reference service is constructed with a null, zero, or negative duration
- **THEN** construction fails before any operation can be submitted
### Requirement: Post-submission communication and interruption policy
If independent confirmation fails with an I/O error after the signature is known, the service SHALL throw an `IOException` whose message identifies the swap operation, includes the signature, and states that the on-chain outcome is unknown, with the original error retained as its cause. Interruption SHALL propagate directly as `InterruptedException` without another RPC or Jupiter request. Neither condition SHALL trigger rebuilding, re-signing, retrying, or resubmitting a transaction.
#### Scenario: Confirmation communication fails
- **WHEN** independent confirmation throws an I/O error for a known submitted signature
- **THEN** the service reports the signature and unknown on-chain outcome while preserving the original error and performs no retry or resubmission
#### Scenario: Confirmation is interrupted
- **WHEN** independent confirmation throws `InterruptedException`
- **THEN** that interruption propagates directly and the service performs no subsequent request
@@ -0,0 +1,22 @@
## 1. Public Jupiter API
- [x] 1.1 Add the checked, documented `JupiterTransactionOutcomeException` with validated structured signature, commitment, and failed/timed-out outcome access.
- [x] 1.2 Update Jupiter Swap and Perps public Javadoc and checked-exception declarations through their API call chains.
## 2. Jupiter Service Confirmation
- [x] 2.1 Add a validated confirmation-timeout overload to `JupiterSwapServiceImpl`, retain its two-minute default constructor, and await validated Swap results at `CONFIRMED` without changing provider validation or submission behavior.
- [x] 2.2 Add a validated confirmation-timeout overload to `AnchorIdlJupiterPerpsServiceImpl`, retain its two-minute default constructor, and route increase and decrease signatures through one private `CONFIRMED` outcome-policy helper.
- [x] 2.3 Ensure both services preserve structured `FAILED` and `TIMED_OUT` outcomes, contextualize confirmation `IOException`s as unknown outcomes, propagate interruption directly, and perform no retry or resubmission.
## 3. Perps Alarm Reporting
- [x] 3.1 Update the increase alarm action to report `FAILED` and `TIMED_OUT` distinctly while leaving confirmation ownership in the Perps service.
- [x] 3.2 Update the decrease alarm action to report `FAILED` and `TIMED_OUT` distinctly while leaving confirmation ownership in the Perps service.
## 4. Verification
- [x] 4.1 Compile main and test source sets without adding tests or executing live Jupiter/Solana transactions.
- [x] 4.2 Run available relevant existing tests only if their runtime is non-live and practical.
- [x] 4.3 Run strict OpenSpec validation and `git diff --check`.
- [x] 4.4 Review the complete diff for checked-exception propagation, unchanged provider validations, exact timeout forwarding, direct interruption propagation, no retry/resubmission, no generated-source edits, and no unrelated changes.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,55 @@
## Context
The current SPL transfer flow separates responsibilities cleanly: `SolanaBlockChain` serializes unsigned low-level transactions, while `SolanaWallet` resolves mint and holding state, validates human-readable amounts, signs, and submits. The transaction serializer already supports legacy-message account ordering, compact-u16 encoding, recent blockhash metadata, and selecting either supported SPL token program.
## Goals / Non-Goals
**Goals:**
- Extend the existing API/implementation boundary with checked SPL-token burning.
- Keep all state-dependent ownership and balance checks in the wallet and all wire serialization in the blockchain implementation.
- Use identical behavior for the original SPL Token Program and Token-2022 except for the selected program address.
- Preserve the existing submission-only signing flow.
**Non-Goals:**
- Confirmation awaiting, retry, account closure, rent recovery, multisignatures, delegates, or permanent delegates.
- Native or wrapped SOL burning, a burn-all convenience API, or integration with Evelyn and Discord.
- New ValueTags, configuration, dependencies, tests, or changes to unrelated consumers.
## Decisions
### Mirror the existing transfer responsibility split
The wallet will reuse its existing mint-program detection, supply loading, holding lookup, decimal consistency checks, exact decimal conversion, and `signAndSendTransaction()` flow. The blockchain method accepts already-resolved raw units and program identity and only validates/serializes those low-level inputs.
Moving state lookup into the transaction builder was rejected because it would duplicate wallet policy and blur the existing API boundary. Building the transaction directly in the wallet was rejected because low-level Solana serialization belongs to `SolanaBlockChain`.
### Encode one standard BurnChecked instruction
The unsigned legacy transaction will contain four account keys in privilege order: owner as writable signer/fee payer, token account and mint as writable unsigned accounts, and the selected token program as read-only unsigned. Its one compiled instruction will invoke program index 3 with account indexes token account, mint, owner and data consisting of the `BurnChecked` discriminator, little-endian unsigned-u64 amount bits, and unsigned-byte decimals. The message declares one required signature, zero read-only signed accounts, and one read-only unsigned account.
This follows the repository's current `TransferChecked` serializer instead of introducing a transaction framework or external dependency. Although Java `long` is signed, a validated unsigned-u64 `BigInteger` is serialized through its low 64 bits with `longValue()`, preserving the complete bit pattern.
### Validate all local builder inputs before RPC access
The builder will parse and range-check the raw amount, range-check decimals, require a non-null supported enum program, and decode all supplied addresses before requesting a recent blockhash. This keeps invalid input deterministic and avoids unnecessary RPC traffic. The same shared address decoder used by existing transaction builders provides Base58 and 32-byte validation.
### Treat complete-balance burn as an ordinary amount
Wallet balance validation rejects only raw amounts greater than the holding, so equality is deliberately allowed. No close instruction is appended; the token account remains open with a zero balance.
### Delegate construction through the cache boundary
`CachedSolanaBlockChain` will forward the call directly because each build requires a fresh recent blockhash. Caching or deduplicating transaction construction could return stale blockhashes and is therefore unsafe.
## Risks / Trade-offs
- [Hand-written Solana wire encoding can be sensitive to account ordering and instruction discriminators] → Mirror the existing compiled-instruction serializer and use the standard `BurnChecked` layout with explicit constants.
- [Unsigned-u64 values above signed-long maximum appear negative as Java longs] → Validate with `BigInteger` first and serialize the low 64-bit two's-complement representation in little-endian order.
- [Submission success does not establish on-chain burn success] → Keep the API explicitly submission-only; callers may separately await the returned signature when their domain policy requires it.
- [Mint and holding state can change between validation and execution] → Let Solana's checked instruction and runtime enforce final state; do not retry automatically.
## Migration Plan
Add the API methods, implementations, and direct cached delegation in one source-compatible change. Existing callers need no migration because the new methods are additive. Rollback removes those additions; no data or configuration migration is involved.
@@ -0,0 +1,25 @@
## Why
Future Evelyn functionality needs a generic way to burn wallet-owned SPL tokens, including EVE under Token-2022. The existing wallet/blockchain separation for SPL transfers provides the right boundary for adding checked burn construction and a high-level validate-sign-submit operation without coupling confirmation or account cleanup to submission.
## What Changes
- Add a public blockchain operation that builds one unsigned `BurnChecked` SPL-token transaction for either the legacy SPL Token Program or Token-2022.
- Add a public wallet operation that validates blockchain state and an exact human-readable burn amount, then builds, signs, and submits the transaction once.
- Delegate burn transaction construction directly through the cached blockchain decorator without caching.
- Preserve submission-only behavior: no confirmation awaiting, retry, token-account closure, or rent recovery.
- Reuse the existing `ΩRawAmountΩ`, `ΩamountDecimalsΩ`, address, amount, and transaction ValueTags without changing Detag configuration.
## Capabilities
### New Capabilities
- `solana-spl-token-burning`: Defines safe checked SPL-token burn construction and wallet-owned submission for legacy SPL Token and Token-2022 mints.
### Modified Capabilities
None.
## Impact
The public `SolanaBlockChain` and `SolanaWallet` APIs gain burn operations. Their reference implementations and `CachedSolanaBlockChain` gain corresponding construction, validation, submission, and direct-delegation behavior. No consumers, configuration, tests, generated sources, or unrelated services change.
@@ -0,0 +1,72 @@
## Purpose
Defines safe construction and wallet submission of checked SPL-token burns for both supported Solana token programs.
## ADDED Requirements
### Requirement: Checked SPL-token burn transaction construction
The blockchain API SHALL build an unsigned transaction containing exactly one checked SPL-token burn instruction for a supplied owner, token account, mint, raw amount, decimal count, and supported token program. The owner SHALL be the fee payer, token-account authority, and sole required signer. The transaction SHALL burn from the supplied token account and mint, use a recent blockhash, and remain unsigned and unsubmitted.
#### Scenario: Legacy SPL Token burn construction
- **WHEN** valid burn inputs identify the original SPL Token Program
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by that program with the supplied account, mint, amount, and decimals
#### Scenario: Token-2022 burn construction
- **WHEN** valid burn inputs identify Token-2022
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by Token-2022 with the supplied account, mint, amount, and decimals
#### Scenario: Builder does not submit
- **WHEN** a checked burn transaction is built successfully
- **THEN** the blockchain returns the unsigned serialized transaction and recent blockhash metadata without signing or submitting it
### Requirement: Low-level burn input validation
Before fetching a blockhash, the blockchain SHALL reject null, blank, malformed, or unsupported required arguments. The raw amount SHALL be a valid positive integer representable as an unsigned 64-bit value, and the decimal count SHALL be representable as an unsigned byte.
#### Scenario: Invalid raw amount
- **WHEN** the raw amount is null, non-numeric, zero, negative, or greater than unsigned 64-bit maximum
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid decimals
- **WHEN** the decimal count is less than zero or greater than 255
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid address or program
- **WHEN** the owner, token account, or mint is null, blank, malformed, or does not decode to a Solana address, or the token program is null
- **THEN** transaction construction fails locally before fetching a blockhash
### Requirement: Wallet-owned burn validation and exact conversion
The wallet SHALL accept a mint and positive human-readable amount, load the mint account, detect whether it is owned by the original SPL Token Program or Token-2022, obtain the mint decimals, and find its own holding under that program. It SHALL require the holding decimals to equal the mint decimals and SHALL convert the requested amount exactly to positive raw units without rounding. It SHALL reject unsupported mints, missing or inconsistent state, excessive precision, and amounts exceeding the wallet balance before transaction construction or submission.
#### Scenario: Exact partial balance burn
- **WHEN** a supported mint and positive amount are exactly representable with the mint decimals and do not exceed the wallet holding
- **THEN** the wallet supplies the corresponding exact raw amount, its own token account, detected program, and mint decimals to checked burn construction
#### Scenario: Complete balance burn
- **WHEN** the exact raw burn amount equals the wallet's complete token balance
- **THEN** the wallet permits the burn without requesting token-account closure or rent recovery
#### Scenario: Amount has excessive precision
- **WHEN** the requested human-readable amount contains a non-zero fraction beyond the mint decimal precision
- **THEN** the wallet rejects it with `IllegalArgumentException` before building, signing, or submitting a transaction
#### Scenario: Blockchain state cannot authorize burn
- **WHEN** the mint is missing or unsupported, the wallet has no holding, the holding decimals differ from the mint decimals, or the amount exceeds the holding balance
- **THEN** the wallet rejects the operation with `IllegalStateException` before building, signing, or submitting a transaction
### Requirement: Single submission without automatic completion handling
After successful validation, the wallet SHALL build the burn through the blockchain API, sign it through its existing signing flow, submit it exactly once through its existing submission flow, and return the resulting transaction signature. It SHALL return immediately after submission without awaiting confirmation, retrying, rebuilding, re-signing, resubmitting, or closing the token account.
#### Scenario: Burn is submitted successfully
- **WHEN** validation, construction, signing, and submission succeed
- **THEN** the wallet returns the transaction signature from the single submission without invoking transaction awaiting
#### Scenario: Burn flow is interrupted or fails
- **WHEN** blockchain access, signing, or submission throws an I/O error or the calling thread is interrupted
- **THEN** the corresponding exception propagates and no automatic retry or subsequent completion action occurs
### Requirement: Cached blockchain delegates burn construction
The cached blockchain decorator SHALL delegate every burn transaction construction call directly to its underlying blockchain without caching, reuse, or deduplication.
#### Scenario: Repeated construction through cached decorator
- **WHEN** callers request burn transaction construction multiple times through the cached decorator
- **THEN** every invocation reaches the underlying blockchain independently
@@ -0,0 +1,22 @@
## 1. Public APIs
- [x] 1.1 Add the documented checked-burn transaction builder to `SolanaBlockChain` using the existing burn input ValueTags.
- [x] 1.2 Add the documented human-readable SPL-token burn operation to `SolanaWallet` with submission-only semantics.
## 2. Blockchain Construction
- [x] 2.1 Implement pre-RPC raw amount, decimal, program, and Solana address validation in `SolanaBlockChainImpl`.
- [x] 2.2 Serialize one unsigned `BurnChecked` transaction with owner authority/fee-payer semantics, a recent blockhash, and selectable legacy SPL Token or Token-2022 program.
- [x] 2.3 Add direct, uncached burn-construction delegation to `CachedSolanaBlockChain`.
## 3. Wallet Burn Flow
- [x] 3.1 Implement mint lookup, supported-program detection, supply/holding lookup, decimal consistency, and exact human-to-raw amount conversion in `SolanaWalletImpl`.
- [x] 3.2 Enforce missing-state and balance failures while permitting an exact complete-balance burn without closing the token account.
- [x] 3.3 Build, sign, and submit exactly once through existing flows, return the signature immediately, and add no awaiting, retry, or resubmission.
## 4. Verification
- [x] 4.1 Compile main and test source sets without adding or modifying tests and without executing a live burn or submission.
- [x] 4.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 4.3 Review the complete diff for both token programs, exact amount conversion, complete-balance support, direct cached delegation, submission-only behavior, no generated/test changes, and no unrelated modifications.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,54 @@
## Context
See `proposal.md` for motivation. The current notification framework combines an API package and three implementation packages under `com.r35157.libs.notification`; its generic message wrappers leak transport choices into consumers. The code currently has no external production consumer, but each implementation contains an inline compile-time usage example. The separate alarm-specific Pushover action is intentionally outside this module and must remain unchanged.
The repository temporarily hosts immature API and implementation artifacts together. Package ownership must nevertheless express a notification-service module that can later be extracted, with API types separated from transport implementations.
## Goals / Non-Goals
**Goals:**
- Establish a small stable API package containing only the bound service, addressed service, and destination abstraction.
- Make transport implementations consume the three existing String-backed ValueTags directly.
- Preserve the existing Pushover HTTP request and synchronous error behavior while enforcing message validation before transport work.
- Make SMTP's complete destination configuration immutable and bindable before injection into a consumer.
**Non-Goals:**
- Implementing Discord or SMTP delivery.
- Adding notification configuration, asynchronous delivery, retries, queues, persistence, or timeout policies.
- Refactoring the alarm-specific Pushover implementation or changing `NenjimHubImpl.startAutoRunProcesses()`.
- Adding or modifying automated tests.
## Decisions
### Separate API and transport implementation packages under the service owner
The API will live directly in `com.r35157.service.notification`, while Discord, Pushover, and SMTP remain in `impl.discord`, `impl.pushover`, and `impl.smtp` below it. This follows the repository's future module boundaries while keeping the current single-project layout. Retaining aliases in the old package was rejected because it would prolong the incompatible abstraction and violates the issue's clean migration requirement.
### Represent messages directly with the semantic ValueTag
All push contracts use `ΩNotificationMessageΩ` directly. Validation is performed at each concrete public push entry point before any delivery work. A replacement record, marker hierarchy, or transport generic was rejected because it would recreate the coupling the change removes.
### Keep addressed binding minimal and destination-only
`AddressedNotificationService<D>` captures a destination in its default `bind(D)` result. `SMTPDestination` captures both email address and subject and validates both in its compact constructor. `SMTPNotificationService` adds a convenience overload accepting these two values and delegates to the standard destination binding. Keeping the subject on each message was rejected because it would leave the service only partially configured when injected.
### Preserve transport maturity independently
Pushover keeps its current form encoding, endpoint, synchronous HTTP call, status handling, and `IOException` contract. Discord and SMTP validate message input first and then retain their `UnsupportedOperationException` behavior for otherwise valid messages. This prevents a refactor from silently expanding either transport's behavior.
## Risks / Trade-offs
- **[Breaking source migration]** All old names and packages disappear at once. → Migrate every production reference and inline example in the same change, then structurally search for old names.
- **[Validation duplicated across implementations]** Each implementation owns a small null/blank guard. → Prefer explicit local validation over adding an unnecessary public utility or runtime message wrapper; keep the exception type and condition identical.
- **[No automated regression coverage]** The issue explicitly excludes test changes. → Compile both source sets, run the normal assembly, and perform strict structural checks of packages, types, call sites, and generated-source boundaries.
## Migration Plan
1. Introduce the renamed API and transport implementations in the new package using the existing ValueTags.
2. Update all production imports and inline examples to the new contracts.
3. Remove the complete old package and every obsolete notifier/message type without aliases.
4. Compile and assemble from `.tjava`, validate OpenSpec strictly, and verify the source tree contains no stale API references.
Rollback consists of reverting the complete source and OpenSpec change together; the old and new APIs are intentionally not supported in parallel.
@@ -0,0 +1,25 @@
## Why
The existing notification API exposes transport-specific message types to consumers, so a fully configured notification sender cannot be substituted without changing consumer code. Before adding further transports, the framework needs a stable service API in which configuration is bound at composition time and consumers provide only a semantically tagged text message.
## What Changes
- **BREAKING** Relocate the notification API and implementations from `com.r35157.libs.notification` to `com.r35157.service.notification`, without compatibility aliases.
- **BREAKING** Replace `BoundNotifier` and `AddressedNotifier` with transport-independent `BoundNotificationService` and destination-only generic `AddressedNotificationService` contracts.
- **BREAKING** Remove the marker and transport-specific message wrappers in favor of `ΩNotificationMessageΩ` directly.
- Rename the Pushover, Discord, and SMTP implementations to notification-service terminology while preserving their current delivery status.
- Validate every notification message before delivery, and make SMTP binding capture both a validated email address and fixed subject.
## Capabilities
### New Capabilities
- `notification-services`: Defines the transport-independent notification API, binding and validation behavior, and the required behavior of the existing Pushover, Discord, and SMTP implementations.
### Modified Capabilities
None.
## Impact
The change replaces the public notification package and its API types, updates all production imports and inline examples, and removes obsolete message classes. Pushover's synchronous HTTP delivery remains functional; Discord webhook and SMTP delivery remain unimplemented. Alarm-specific Pushover code and Nenjim startup composition are unaffected.
@@ -0,0 +1,78 @@
## Purpose
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
## ADDED Requirements
### Requirement: Transport-independent bound notification contract
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
#### Scenario: Bound consumer sends a message
- **WHEN** a consumer receives a configured `BoundNotificationService`
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
#### Scenario: Delivery failure is reported synchronously
- **WHEN** a bound transport encounters a delivery failure
- **THEN** `push(message)` reports the failure through its `IOException` contract
### Requirement: Addressed services bind destination configuration
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
#### Scenario: Destination is bound once
- **WHEN** a destination is passed to an addressed notification service's binding operation
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
### Requirement: Notification message validation
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
#### Scenario: Null message is rejected
- **WHEN** a caller pushes a null notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
#### Scenario: Empty or blank message is rejected
- **WHEN** a caller pushes an empty or whitespace-only notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
### Requirement: Pushover delivery remains functional
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
#### Scenario: Valid Pushover message is submitted
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
- **THEN** the service submits the same message through the existing Pushover API request flow
#### Scenario: Pushover rejects delivery
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
- **THEN** the service reports the delivery failure as an `IOException`
### Requirement: SMTP binding captures address and subject
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
#### Scenario: SMTP service is fully bound
- **WHEN** a caller binds a valid email address and subject
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
#### Scenario: Invalid SMTP binding is rejected
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
- **THEN** the operation throws `IllegalArgumentException`
#### Scenario: Valid SMTP delivery remains unimplemented
- **WHEN** a valid message is pushed through a valid SMTP binding
- **THEN** the service reports that SMTP delivery is not implemented
### Requirement: Discord delivery remains unimplemented
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
#### Scenario: Valid Discord delivery remains unimplemented
- **WHEN** a valid message is pushed through the configured Discord service
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
### Requirement: Obsolete notification API is removed
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
#### Scenario: Production code uses the new service API
- **WHEN** the production source tree is inspected after migration
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
#### Scenario: Removed types are unavailable
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
- **THEN** that type is no longer part of the source or compiled API
@@ -0,0 +1,17 @@
## 1. Notification API Migration
- [x] 1.1 Add the documented `BoundNotificationService`, `AddressedNotificationService`, and `NotificationDestination` API under `com.r35157.service.notification`.
- [x] 1.2 Remove the old API package, marker interface, notifier contracts, and all compatibility aliases.
## 2. Transport Implementations
- [x] 2.1 Migrate and rename Pushover to `PushoverNotificationService`, accept and validate `ΩNotificationMessageΩ`, and preserve the existing delivery flow.
- [x] 2.2 Migrate and rename Discord to `DiscordNotificationService`, validate direct messages, and preserve its unimplemented delivery status.
- [x] 2.3 Migrate SMTP to `SMTPNotificationService` with a validated address-and-subject `SMTPDestination`, direct binding convenience, direct message validation, and unimplemented delivery status.
- [x] 2.4 Update every production call site and inline example, then confirm the old package and removed types have no remaining production references.
## 3. Verification
- [x] 3.1 Compile main and test source sets and complete the repository's normal assembly/build without changing tests.
- [x] 3.2 Strictly validate the OpenSpec change and run whitespace/diff checks.
- [x] 3.3 Review the complete diff for scope, generated-source changes, test changes, transport-independent consumer dependencies, and preservation of excluded services.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-12
@@ -0,0 +1,45 @@
## Context
See `proposal.md` for motivation. `DiscordNotificationService` is the only affected production class and currently validates only null/blank messages before throwing `UnsupportedOperationException`. The existing Jackson dependency can safely serialize the payload; no notification API, caller, or composition-root change is required.
## Goals / Non-Goals
**Goals:**
- Construct a fully bound synchronous Discord webhook service from `ΩRestEndpointΩ` and a positive `Duration`.
- Validate the endpoint locally, preserve message text exactly, and make successful response handling explicit.
- Keep the webhook URI confidential in all validation and delivery paths.
**Non-Goals:**
- Configuration loading or Nenjim composition-root wiring.
- Retries, rate-limit waiting, asynchronous execution, or non-text Discord features.
- Changes to the notification API, Pushover, SMTP, alarms, Evelyn, or tests.
## Decisions
### Strict local URI validation before client construction
The constructor validates the URI shape with URI components and a fixed path grammar: `https://discord.com/api/webhooks/{id}/{token}` or `https://discord.com/api/v{version}/webhooks/{id}/{token}`. Versioned paths accept only Discord API versions 6 through 10, which are currently non-discontinued; the host is case-insensitive and all token-bearing endpoint values are absent from diagnostics. A network probe is rejected because construction must be side-effect free and valid webhooks can change remotely.
### One synchronous JSON request per valid push
The service uses a construction-time `HttpClient` configured with the supplied connect timeout and `Redirect.NEVER`. Every valid message is serialized with Jackson into `content`, then posted with `wait=true`, the same request timeout, JSON headers, and a `DiscordBot (url, version)` User-Agent identifying Nenjim. This preserves the BoundNotificationService contract and lets Discord confirm persistence. Manual JSON escaping and asynchronous delivery are rejected.
### Failure policy preserves secrets and interruption
Only 2xx is successful. Other statuses become `IOException` containing only status and useful body. I/O failures are wrapped with non-sensitive context; interruption restores the flag and is the cause of an `IOException`. The service neither retries nor waits after 429, so each valid `push()` owns exactly one send attempt.
## Risks / Trade-offs
- **[Webhook endpoint is a credential]** → Never log or interpolate it into exceptions; redirect following is disabled.
- **[Strict URI validation could reject a future Discord route]** → Accept canonical unversioned and conventional versioned execute-webhook paths only; future route support requires an explicit spec change.
- **[No automated tests by issue instruction]** → Compile generated sources and unchanged tests, assemble, validate OpenSpec, and perform structural and diff checks without a live webhook.
## Migration Plan
1. Replace the URL-based stub constructor and delivery path in place.
2. Update the Discord requirement in the active capability delta.
3. Verify compilation, assembly, strict OpenSpec validation, and absence of old-stub patterns or credentials.
Rollback is a single-class and active-change revert; no consumer migration is needed.
@@ -0,0 +1,24 @@
## Why
The transport-independent notification API already exposes a bound Discord service, but its valid-message path is still a stub. Completing synchronous Discord incoming-webhook delivery makes that service usable without exposing Discord details to consumers.
## What Changes
- Replace the Discord stub with synchronous JSON webhook delivery through the existing bound notification-service API.
- Require a structurally valid canonical Discord webhook URI and a positive caller-supplied timeout at construction.
- Validate messages, preserve their content exactly, use `wait=true`, and report delivery failures through `IOException` without retrying.
- Protect webhook credentials from diagnostics and disable redirect following.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `notification-services`: Replaces the unimplemented Discord-delivery requirement with fully bound synchronous webhook-delivery, validation, response, interruption, and secret-protection behavior.
## Impact
Only `DiscordNotificationService` and the active OpenSpec change are affected. The existing Jackson dependency is reused. No composition-root wiring, other notification transport, or test code changes.
@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Discord webhook delivery
The Discord notification service SHALL remain a `BoundNotificationService` that is fully configured at construction from a non-null structurally valid canonical Discord incoming-webhook `ΩRestEndpointΩ` and a non-null positive `Duration`. It SHALL make no construction-time network request, follow no HTTP redirects, and SHALL NOT expose a raw `URL` or raw `String` constructor.
The endpoint SHALL be an absolute HTTPS URI with a case-insensitive `discord.com` host, no user-info, fragment, query, or non-HTTPS port, and either an unversioned `/api/webhooks/{id}/{token}` path or a versioned `/api/v{version}/webhooks/{id}/{token}` path using a non-discontinued Discord API version from 6 through 10, with non-blank ID and token segments. Constructor validation diagnostics SHALL NOT disclose the endpoint or token.
Every public `push(...)` entry point SHALL reject a null, empty, blank, or over-2,000-character `ΩNotificationMessageΩ` with `IllegalArgumentException` before JSON serialization, request construction, or transport work. For every valid message, the service SHALL synchronously send exactly one POST to the configured endpoint with `wait=true`, the configured request timeout, JSON UTF-8 content and accept headers, a `DiscordBot (url, version)` User-Agent identifying Nenjim, and a safely serialized JSON object whose `content` preserves the message exactly. The webhook token SHALL be the only authentication mechanism.
Every 2xx HTTP response SHALL return normally. Every non-2xx response, including redirects and 429 responses, and every communication or request-preparation failure SHALL cause `IOException` without retrying, waiting, or sending another request. Response failures SHALL include the HTTP status and useful response body when available, without disclosing the endpoint or token. When synchronous HTTP delivery is interrupted, the service SHALL restore the thread interrupt flag and throw an `IOException` retaining the `InterruptedException` as its cause.
#### Scenario: Fully configured construction
- **WHEN** a caller constructs the service with a canonical Discord webhook endpoint and positive timeout
- **THEN** it receives a bound notification service without a network request
#### Scenario: Invalid endpoint or timeout is rejected
- **WHEN** a caller supplies a null or structurally invalid endpoint, or a null, zero, or negative timeout
- **THEN** construction throws `IllegalArgumentException` without disclosing the endpoint token
#### Scenario: Invalid notification message is rejected before delivery
- **WHEN** a caller pushes a null, empty, blank, or over-2,000-character message
- **THEN** the service throws `IllegalArgumentException` before serialization or HTTP work
#### Scenario: Valid message is synchronously confirmed
- **WHEN** a caller pushes a valid notification message and Discord returns a 2xx response
- **THEN** the service sends one synchronous JSON POST with `wait=true` and returns normally
#### Scenario: Discord rejects delivery
- **WHEN** Discord returns a non-2xx response, including 429 or a redirect
- **THEN** the service throws `IOException` containing the status and does not retry or follow the redirect
#### Scenario: Delivery communication fails
- **WHEN** preparing or sending the webhook request fails
- **THEN** the service throws `IOException` without exposing the webhook endpoint or token
#### Scenario: Delivery is interrupted
- **WHEN** synchronous webhook delivery is interrupted
- **THEN** the service restores the thread interrupt status and throws `IOException` with the interruption as its cause
#### Scenario: Webhook secret remains protected
- **WHEN** validation or delivery reports an error
- **THEN** no log or exception message includes the complete webhook endpoint or token
## REMOVED Requirements
### Requirement: Discord delivery remains unimplemented
**Reason**: Discord incoming-webhook delivery is now implemented synchronously.
**Migration**: Construct `DiscordNotificationService` with a canonical Discord webhook `ΩRestEndpointΩ` and a positive `Duration`, then use it through `BoundNotificationService`.
@@ -0,0 +1,12 @@
## 1. Discord Webhook Delivery
- [x] 1.1 Replace the URL stub constructor with validated `ΩRestEndpointΩ` and positive `Duration` construction, including connect-timeout and no-redirect client configuration.
- [x] 1.2 Implement direct notification-message validation and one synchronous Jackson JSON webhook POST with `wait=true`, headers, request timeout, and exact message content.
- [x] 1.3 Implement 2xx success handling and non-sensitive `IOException` handling for non-2xx, communication, timeout, and interrupted delivery without retries.
- [x] 1.4 Remove or update the private inline example so it cannot carry a credential or perform a network-executable example.
## 2. Verification
- [x] 2.1 Compile generated main sources and existing unchanged test sources, then run the normal assembly/build.
- [x] 2.2 Run strict validation of the active OpenSpec change and structural checks for the old URL constructor, stub, credentials, and prohibited scope changes.
- [x] 2.3 Review the complete diff, confirm no test or generated source changes, and commit the scoped implementation.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-13

Some files were not shown because too many files have changed in this diff Show More