Compare commits
184
Commits
0.0.0
...
ed952fd6b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed952fd6b1 | ||
|
|
0cb125ca9d | ||
|
|
7b1f6d18eb | ||
|
|
f5fc5bb3b5 | ||
|
|
a252c256c4 | ||
|
|
b230e15ece | ||
|
|
f29013e9c2 | ||
|
|
c645ba34eb | ||
|
|
7d1557932d | ||
|
|
c70be47c27 | ||
|
|
f5a2183a40 | ||
|
|
3d5de0e09b | ||
|
|
961f8c69cb | ||
|
|
df504e29bf | ||
|
|
b33db6da49 | ||
|
|
b86ec37bb1 | ||
|
|
cb2d3e2a16 | ||
|
|
e7f551b8dd | ||
|
|
5a096379f2 | ||
|
|
b429a61237 | ||
|
|
21bd57ae7a | ||
|
|
30cfcfc4ff | ||
|
|
d5b733bc6f | ||
|
|
cac555159d | ||
|
|
bc936e029d | ||
|
|
ef1b5e0adc | ||
|
|
0db74b3296 | ||
|
|
d8dc494afa | ||
|
|
d6b765e9a5 | ||
|
|
fdefba91ad | ||
|
|
493a6291a9 | ||
|
|
6ad5ad0924 | ||
|
|
d946508214 | ||
|
|
d9e0afeff9 | ||
|
|
3f5445e072 | ||
|
|
28844bb6db | ||
|
|
2177722fd9 | ||
|
|
da72aa4458 | ||
|
|
70fbb793ec | ||
|
|
b9741d09fd | ||
|
|
847c33000a | ||
|
|
a65a9ed043 | ||
|
|
8189b16516 | ||
|
|
8f6aac025c | ||
|
|
6c48e3741a | ||
|
|
2a64809d97 | ||
|
|
b4b087cb3a | ||
|
|
4ffc9a75ae | ||
|
|
bfda6f01b7 | ||
|
|
40ac60acd1 | ||
|
|
1bde6c8152 | ||
|
|
1d63cdc026 | ||
|
|
4b913d4c83 | ||
|
|
280024c340 | ||
|
|
12843e1501 | ||
|
|
4cc14ceef5 | ||
|
|
bf492c9db1 | ||
|
|
f5c449fe23 | ||
|
|
5afac819d8 | ||
|
|
8f2e43dc8a | ||
|
|
f42ac30b3f | ||
|
|
451b5f252c | ||
|
|
0b6b0b6974 | ||
|
|
d4ced47012 | ||
|
|
3ba92bae4b | ||
|
|
5d940d37cc | ||
|
|
d4e6d3438b | ||
|
|
bbba18dcae | ||
|
|
9a770e1801 | ||
|
|
5b50e67d8e | ||
|
|
d2f28af1bc | ||
|
|
e59aff95b8 | ||
|
|
7266a8c2dd | ||
|
|
d33dff7091 | ||
|
|
6a352e90e9 | ||
|
|
2e61011a68 | ||
|
|
ba376b6688 | ||
|
|
a0d0740f10 | ||
|
|
776f626abb | ||
|
|
228698e3e4 | ||
|
|
0166d6bcf7 | ||
|
|
87ef7c67ae | ||
|
|
e0713c9121 | ||
|
|
5c9f6251e5 | ||
|
|
f3e4947086 | ||
|
|
db20e68e93 | ||
|
|
eda12a12fd | ||
|
|
b2278b12d5 | ||
|
|
27c161bb6b | ||
|
|
f6afbd322b | ||
|
|
3e82b49f0d | ||
|
|
397028fe37 | ||
|
|
2c57b4cdb5 | ||
|
|
724b2d6eb6 | ||
|
|
aded93839b | ||
|
|
16fd99c80f | ||
|
|
e5b60f9db2 | ||
|
|
5c8da8bf8a | ||
|
|
ff96949fdc | ||
|
|
d4cac8d0bd | ||
|
|
82331e068a | ||
|
|
8f86773786 | ||
|
|
041909ae6a | ||
|
|
b21b0b081d | ||
|
|
0997c0558e | ||
|
|
eda11ed459 | ||
|
|
c05509bdbe | ||
|
|
32a2667e59 | ||
|
|
b8da2fb0f5 | ||
|
|
57875d1eeb | ||
|
|
23b644171b | ||
|
|
265a30bfa3 | ||
|
|
9e41499501 | ||
|
|
c97469a9fe | ||
|
|
5375309b08 | ||
|
|
5063a6e523 | ||
|
|
cdcbd331ec | ||
|
|
7d96848464 | ||
|
|
7075429efa | ||
|
|
329e73f3f8 | ||
|
|
ee9e4d9194 | ||
|
|
b7490b8482 | ||
|
|
1546db773f | ||
|
|
ecb0b265b5 | ||
|
|
d18190964c | ||
|
|
832785ed4d | ||
|
|
f874820aa3 | ||
|
|
fb1327f62e | ||
|
|
2445fe2722 | ||
|
|
794904bcf1 | ||
|
|
c57417288a | ||
|
|
3a17604491 | ||
|
|
a63f8776f2 | ||
|
|
e2fbc1a219 | ||
|
|
d98c5fff34 | ||
|
|
c0eca5a56d | ||
|
|
2ad0063c61 | ||
|
|
b083b924b5 | ||
|
|
aa7b3cf9d4 | ||
|
|
ff5f1456f4 | ||
|
|
4e1ba0edc1 | ||
|
|
97f0fdaedb | ||
|
|
f634336290 | ||
|
|
d1445f461e | ||
|
|
6bf35de5c6 | ||
|
|
b6fa7d5240 | ||
|
|
aff5fda1e1 | ||
|
|
aba9344b7e | ||
|
|
034f1d969d | ||
|
|
fe265aa523 | ||
|
|
3f3eb82fe6 | ||
|
|
62e8563cbd | ||
|
|
bb0a43a31f | ||
|
|
3e047ccf2d | ||
|
|
b51db22ad1 | ||
|
|
bff1fd354a | ||
|
|
115d9ca0ee | ||
|
|
179840d0ab | ||
|
|
c0cc65751b | ||
|
|
cead475afd | ||
|
|
cf73e791db | ||
|
|
578e2cf31b | ||
|
|
35d587ef98 | ||
|
|
b087052f59 | ||
|
|
04b3fec8f2 | ||
|
|
f9ed9ea00b | ||
|
|
af473f2408 | ||
|
|
e63e20955a | ||
|
|
677ff96616 | ||
|
|
d29014d806 | ||
|
|
6add00d399 | ||
|
|
8348094736 | ||
|
|
fec65a0e36 | ||
|
|
89b6a4f28d | ||
|
|
4e94320e81 | ||
|
|
2aee555d3c | ||
|
|
c22dfb4e05 | ||
|
|
b0f5430fe0 | ||
|
|
8ab1947a08 | ||
|
|
8b242c2820 | ||
|
|
938894ea26 | ||
|
|
7d113b4522 | ||
|
|
642b531553 | ||
|
|
88f147d46e |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
+12
@@ -3,3 +3,15 @@
|
|||||||
|
|
||||||
# Ignore Gradle build output directory
|
# Ignore Gradle build output directory
|
||||||
build
|
build
|
||||||
|
|
||||||
|
logs/*.log
|
||||||
|
logs/*.log.gz
|
||||||
|
conf/*.conf
|
||||||
|
conf/*.xml
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -11,6 +11,39 @@ This module was initially created with the following command:
|
|||||||
## Usage
|
## Usage
|
||||||
Explain how to use this module
|
Explain how to use this module
|
||||||
|
|
||||||
|
## Configuration format versions
|
||||||
|
|
||||||
|
Every application-owned, human-maintained runtime configuration file starts
|
||||||
|
with an explicit format version:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FORMAT_VERSION=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Blank lines and full-line comments may precede the declaration, but it must be
|
||||||
|
the first actual configuration entry. Loading fails before any other content
|
||||||
|
is parsed when the declaration is missing, duplicated, malformed, misplaced,
|
||||||
|
or different from the version supported by the corresponding loader.
|
||||||
|
|
||||||
|
Versions are independent for each configuration format. A breaking change to
|
||||||
|
one format, such as `alarms.conf`, does not change any other format's version.
|
||||||
|
When a breaking format change is made, update both the loader's hardcoded
|
||||||
|
`SUPPORTED_FORMAT_VERSION` and the declaration in the runtime and example
|
||||||
|
configuration files. Compatible additions do not require a version increment.
|
||||||
|
|
||||||
|
This policy currently applies to:
|
||||||
|
|
||||||
|
- `alarms.conf`
|
||||||
|
- `alarmaction_Console.conf`
|
||||||
|
- `alarmaction_Pushover.conf`
|
||||||
|
- `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf`
|
||||||
|
- `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf`
|
||||||
|
- `evelyn.conf`
|
||||||
|
- `positions.conf`
|
||||||
|
|
||||||
|
Third-party configuration formats such as Log4j's XML configuration are
|
||||||
|
versioned by their owning library and do not use this declaration.
|
||||||
|
|
||||||
## Release log:
|
## Release log:
|
||||||
| Date | Version | Description |
|
| Date | Version | Description |
|
||||||
|:-----------------|:--------------:|----------------------|
|
|:-----------------|:--------------:|----------------------|
|
||||||
|
|||||||
+529
@@ -0,0 +1,529 @@
|
|||||||
|
# Jupiter Perps Price Alarm
|
||||||
|
|
||||||
|
The Jupiter Perps Price Alarm monitors the Jupiter Perps aggregated oracle accounts for SOL, ETH, and BTC. It evaluates configurable price conditions and dispatches matching alarms to independently configured console, Pushover, and Jupiter position-increase or position-decrease actions.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The NenjimHub currently constructs and registers the alarm as
|
||||||
|
`jupiter-perps-alarm.default`, but deliberately does not start it. Its
|
||||||
|
`SolanaWallet` and `JupiterPerpsService` are selected by the Hub's hardcoded
|
||||||
|
composition 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 alarm performs the following steps before it starts monitoring prices:
|
||||||
|
|
||||||
|
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 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 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.6.1 through the Gradle wrapper
|
||||||
|
- Java preview features
|
||||||
|
- Detag-generated Java sources from `src/main/tjava`
|
||||||
|
|
||||||
|
Compile the project with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew classes
|
||||||
|
```
|
||||||
|
|
||||||
|
To create the application JAR and collect runtime dependencies in `build/libs`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew prepareLibs
|
||||||
|
```
|
||||||
|
|
||||||
|
There is currently no alarm-specific automated self-test task.
|
||||||
|
|
||||||
|
## Running through Nenjim
|
||||||
|
|
||||||
|
The alarm no longer has a standalone `main` method. The Registry service
|
||||||
|
manager constructs and registers the wallet, Jupiter service and alarm, but the
|
||||||
|
alarm is intentionally absent from the current explicit application startup
|
||||||
|
list. Running the Hub therefore leaves the registered alarm inactive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew run
|
||||||
|
```
|
||||||
|
|
||||||
|
After `./gradlew prepareLibs`, the same Registry-managed Hub can be invoked with
|
||||||
|
its main class (the alarm still remains inactive):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java --enable-preview \
|
||||||
|
-cp "build/libs/*" \
|
||||||
|
com.r35157.nenjim.hubd.Main
|
||||||
|
```
|
||||||
|
|
||||||
|
The current manager composition binds the alarm to `conf/alarms.conf` and to the
|
||||||
|
test Jupiter Perps wallet and service. Until Nenjim provides configured
|
||||||
|
injection and activation, a different configuration, wallet, or startup choice
|
||||||
|
must be selected in the Registry manager's composition code, not in an alarm
|
||||||
|
action file.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
The Registry manager does not currently forward alarm-specific command-line
|
||||||
|
options. Solana WebSocket endpoints can be overridden with:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
The application uses five configuration files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `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` | 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.
|
||||||
|
|
||||||
|
Example files:
|
||||||
|
|
||||||
|
- [alarms.conf.example](conf/alarms.conf.example)
|
||||||
|
- [alarmaction_Console.conf.example](conf/alarmaction_Console.conf.example)
|
||||||
|
- [alarmaction_Pushover.conf.example](conf/alarmaction_Pushover.conf.example)
|
||||||
|
- [alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example)
|
||||||
|
- [alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example)
|
||||||
|
|
||||||
|
### Format version
|
||||||
|
|
||||||
|
Every configuration file must declare its own format version as its first
|
||||||
|
actual configuration entry:
|
||||||
|
|
||||||
|
| 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
|
||||||
|
before parsing the remaining configuration.
|
||||||
|
|
||||||
|
Each file format has an independent version. Increment only the affected
|
||||||
|
format when making an incompatible change, and update both its parser constant
|
||||||
|
and its runtime and example files. See
|
||||||
|
[Configuration format versions](README.md#configuration-format-versions) for
|
||||||
|
the project-wide policy.
|
||||||
|
|
||||||
|
## Shared variables
|
||||||
|
|
||||||
|
### Variable definitions
|
||||||
|
|
||||||
|
Variable definitions live in `alarms.conf`. A definition consists of a name and a single-token value:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{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}}`.
|
||||||
|
|
||||||
|
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}}
|
||||||
|
{{SOL_SHORT_ENTRY_PRICE}} {{SOL_SHORT_LIQ_PRICE}}
|
||||||
|
{{BTC_LONG_ENTRY_PRICE}} {{BTC_LONG_LIQ_PRICE}}
|
||||||
|
{{BTC_SHORT_ENTRY_PRICE}} {{BTC_SHORT_LIQ_PRICE}}
|
||||||
|
{{ETH_LONG_ENTRY_PRICE}} {{ETH_LONG_LIQ_PRICE}}
|
||||||
|
{{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 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 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
|
||||||
|
|
||||||
|
Each alarm is one line with four columns:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ID ASSET CONDITION TRIGGER
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
|
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported column values:
|
||||||
|
|
||||||
|
- `ID`: integer alarm identifier. IDs should be unique. Action files use this ID to select alarms.
|
||||||
|
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
||||||
|
- `CONDITION`: a comparison or range expression. It must be one token with no whitespace.
|
||||||
|
- `TRIGGER`: `ONETIME`, `CROSSING`, `PERSISTENT`, or `PERSISTENT:<duration>`.
|
||||||
|
|
||||||
|
A trailing comment is allowed after a variable value or trigger.
|
||||||
|
|
||||||
|
An alarm can be listed in zero, one, or several action files. Absence means that action ignores the alarm; absence from all action files means that the condition is monitored but produces no action.
|
||||||
|
|
||||||
|
## Condition expressions
|
||||||
|
|
||||||
|
A condition is evaluated against the current oracle price in USD. Conditions are resolved again for every accepted price event, so periodically refreshed entry and liquidation prices take effect without restarting the application.
|
||||||
|
|
||||||
|
The complete condition must not contain whitespace.
|
||||||
|
|
||||||
|
### Target expressions
|
||||||
|
|
||||||
|
Each target can be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
75.50
|
||||||
|
{{SOL_LONG_LIQ_PRICE}}
|
||||||
|
{{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
{{SOL_SHORT_LIQ_PRICE}}-3%
|
||||||
|
```
|
||||||
|
|
||||||
|
`BASE+P%` adds `P` percent of `BASE`; `BASE-P%` subtracts it. The base, percentage, and resulting target must be zero or positive. General arithmetic expressions are not supported.
|
||||||
|
|
||||||
|
### Comparisons
|
||||||
|
|
||||||
|
| Syntax | Matches when |
|
||||||
|
|---|---|
|
||||||
|
| `<X` | `price < X` |
|
||||||
|
| `<=X` | `price <= X` |
|
||||||
|
| `>X` | `price > X` |
|
||||||
|
| `>=X` | `price >= X` |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<={{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
>={{SOL_SHORT_LIQ_PRICE}}-1%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ranges
|
||||||
|
|
||||||
|
Ranges use `-->` between a lower and an upper target. Parentheses exclude a boundary; square brackets include it.
|
||||||
|
|
||||||
|
| Syntax | Matches when |
|
||||||
|
|---|---|
|
||||||
|
| `(A-->B)` | `A < price < B` |
|
||||||
|
| `[A-->B)` | `A <= price < B` |
|
||||||
|
| `(A-->B]` | `A < price <= B` |
|
||||||
|
| `[A-->B]` | `A <= price <= B` |
|
||||||
|
|
||||||
|
The lower resolved target must not exceed the upper resolved target.
|
||||||
|
|
||||||
|
Example of two adjacent, non-overlapping long-position conditions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<={{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%]
|
||||||
|
```
|
||||||
|
|
||||||
|
At exactly `LIQ+1%`, only the first condition matches. The corresponding short-position split is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
>={{SOL_SHORT_LIQ_PRICE}}-1%
|
||||||
|
[{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trigger behavior
|
||||||
|
|
||||||
|
On the first accepted oracle price after startup, an already-satisfied `ONETIME` or `PERSISTENT` alarm can trigger immediately. A `CROSSING` alarm only establishes its initial state.
|
||||||
|
|
||||||
|
### `ONETIME`
|
||||||
|
|
||||||
|
- Triggers on the first accepted price that satisfies the condition.
|
||||||
|
- Triggers at most once during the current process lifetime.
|
||||||
|
- Leaving and re-entering the condition does not re-arm it.
|
||||||
|
- Restarting the application re-arms it because alarm state is held only in memory.
|
||||||
|
|
||||||
|
### `CROSSING`
|
||||||
|
|
||||||
|
- Triggers when the condition changes from not satisfied to satisfied.
|
||||||
|
- The first accepted price establishes the initial state and never triggers the alarm.
|
||||||
|
- Remaining inside the condition does not trigger again.
|
||||||
|
- Without hysteresis, leaving the condition re-arms the alarm, so the next entry triggers again.
|
||||||
|
- An optional hysteresis can be appended to the condition as `/H` or `/P%`.
|
||||||
|
- With hysteresis, the alarm remains disarmed until the price reaches the re-arm boundary.
|
||||||
|
- A grace period is not supported; `CROSSING:1m` is rejected.
|
||||||
|
- Only the strict directional comparisons `<` and `>` are supported.
|
||||||
|
- `=`, `<=`, `>=`, and range conditions are rejected during configuration parsing.
|
||||||
|
|
||||||
|
The condition determines the crossing direction. Without hysteresis:
|
||||||
|
|
||||||
|
```text
|
||||||
|
19 SOL >{{SOL_LONG_ENTRY_PRICE}} CROSSING
|
||||||
|
20 SOL <{{SOL_LONG_ENTRY_PRICE}} CROSSING
|
||||||
|
```
|
||||||
|
|
||||||
|
Alarm 19 triggers when the price moves from at or below the entry price to above it. Alarm 20 triggers when the price moves from at or above the entry price to below it. Landing exactly on the entry price is not a crossing.
|
||||||
|
|
||||||
|
Hysteresis prevents repeated triggers when the price fluctuates around the target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
21 SOL >{{SOL_LONG_ENTRY_PRICE}}/0.25 CROSSING
|
||||||
|
22 SOL <{{SOL_LONG_ENTRY_PRICE}}/0.25% CROSSING
|
||||||
|
```
|
||||||
|
|
||||||
|
Alarm 21 triggers above the entry price and re-arms only at or below `entry price - 0.25`. Alarm 22 triggers below the entry price and re-arms only at or above `entry price + 0.25%`.
|
||||||
|
|
||||||
|
Percentage hysteresis is calculated from the fully resolved comparison target. Hysteresis is an unsigned distance, so a leading `+` or `-` is rejected. Hysteresis is supported only for `CROSSING` alarms.
|
||||||
|
|
||||||
|
### `PERSISTENT`
|
||||||
|
|
||||||
|
- Triggers immediately on the first accepted matching price.
|
||||||
|
- With no grace period, it can trigger for every accepted matching price event.
|
||||||
|
- `PERSISTENT:<duration>` limits repeated triggering to at most once per configured interval while the condition remains satisfied.
|
||||||
|
- Leaving and re-entering a condition does not bypass the grace period measured from the previous trigger.
|
||||||
|
|
||||||
|
A duration is a non-negative whole number followed by exactly one suffix:
|
||||||
|
|
||||||
|
| Suffix | Unit |
|
||||||
|
|---|---|
|
||||||
|
| `ms` | millisecond |
|
||||||
|
| `s` | second |
|
||||||
|
| `m` | minute |
|
||||||
|
| `h` | hour |
|
||||||
|
| `d` | day |
|
||||||
|
| `w` | week |
|
||||||
|
| `M` | month (30 days) |
|
||||||
|
| `y` | year (365 days) |
|
||||||
|
|
||||||
|
Suffixes are case-sensitive, so `m` means minute and `M` means month. Combined durations such as `1w3d` are not supported.
|
||||||
|
|
||||||
|
Duplicate price events received from redundant RPC endpoints are suppressed using the raw price, exponent, and oracle timestamp. Up to 512 recent event keys are retained per asset monitor.
|
||||||
|
|
||||||
|
## Dynamic price-variable refresh
|
||||||
|
|
||||||
|
Entry-price and liquidation-price variables are refreshed:
|
||||||
|
|
||||||
|
- once during startup, before condition validation;
|
||||||
|
- every 60 seconds in a daemon task;
|
||||||
|
- when `jupiter-perps-alarm-var.refresh` is created in the configuration directory.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch conf/jupiter-perps-alarm-var.refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
The watcher detects creation of the file, refreshes the variables, and then deletes the trigger file.
|
||||||
|
|
||||||
|
When a dynamic price variable changes, the application logs its old and new value.
|
||||||
|
|
||||||
|
## Alarm actions
|
||||||
|
|
||||||
|
Actions are executed sequentially through `CompositeAlarmAction`. A runtime failure in one action is logged so the remaining actions can still run.
|
||||||
|
|
||||||
|
### Console
|
||||||
|
|
||||||
|
`alarmaction_Console.conf` contains one alarm ID per line:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
1
|
||||||
|
4
|
||||||
|
5
|
||||||
|
```
|
||||||
|
|
||||||
|
Only listed alarms are written to the console. The output includes the asset, current oracle price, resolved condition, trigger type, oracle time, slot, and source endpoint. Duplicate or non-integer IDs are rejected. An empty file disables console output for all alarms.
|
||||||
|
|
||||||
|
### Pushover
|
||||||
|
|
||||||
|
`alarmaction_Pushover.conf` contains the credentials followed by zero or more notification definitions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
APPLICATION_TOKEN <pushover-application-token>
|
||||||
|
USER_KEY <pushover-user-key>
|
||||||
|
|
||||||
|
# ID SEVERITY "NOTE"
|
||||||
|
4 CRITICAL "🚨 SOL Long add 1.1x"
|
||||||
|
5 INFO "🌱 SOL Long add 3x"
|
||||||
|
```
|
||||||
|
|
||||||
|
The application token and user/group key are required. Only listed alarm IDs send a Pushover notification. Notes must be quoted and support `\n`, `\r`, `\t`, `\"`, and `\\` escapes.
|
||||||
|
|
||||||
|
| Severity | Pushover parameters |
|
||||||
|
|---|---|
|
||||||
|
| `EMERGENCY` | `priority=2`, `retry=30`, `expire=10800`, `sound=persistent` |
|
||||||
|
| `CRITICAL` | `priority=1`, `sound=spacealarm` |
|
||||||
|
| `WARN` | `priority=0`, `sound=siren` |
|
||||||
|
| `INFO` | `priority=0` |
|
||||||
|
| `SILENT` | `priority=-1` |
|
||||||
|
| `GHOST` | `priority=-2` |
|
||||||
|
|
||||||
|
Credentials, severity, and notes may reference shared variables. Credentials and severity are resolved during startup; notes are also resolved for every trigger so refreshed variables take effect. Pushover requests are sent asynchronously with a 15-second request timeout. Rejected requests and asynchronous failures are logged.
|
||||||
|
|
||||||
|
### Automatic position increase
|
||||||
|
|
||||||
|
`alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains a minimum
|
||||||
|
USDC reserve followed by zero or more position increases:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FORMAT_VERSION=2
|
||||||
|
|
||||||
|
RESERVE_USDC_LIMIT 10
|
||||||
|
|
||||||
|
# ID ASSET DIRECTION COLLATERAL_USDC SIZE_DELTA_USD MAX_SLIPPAGE_BPS
|
||||||
|
4 SOL LONG 0.25 2.50 200
|
||||||
|
```
|
||||||
|
|
||||||
|
Each active row can submit a real transaction when its alarm triggers. Supported values are:
|
||||||
|
|
||||||
|
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
||||||
|
- `DIRECTION`: `LONG` or `SHORT`.
|
||||||
|
- `COLLATERAL_USDC`: greater than zero.
|
||||||
|
- `SIZE_DELTA_USD`: zero or greater.
|
||||||
|
- `MAX_SLIPPAGE_BPS`: from `0` to `10000`.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Provide multiple comma-separated WebSocket endpoints for redundancy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
|
||||||
|
```
|
||||||
|
|
||||||
|
The application creates one WebSocket connection for every configured asset/endpoint combination. With three configured assets and two endpoints, it creates six connections.
|
||||||
|
|
||||||
|
Each connection:
|
||||||
|
|
||||||
|
- subscribes with `processed` commitment and Base64 account encoding;
|
||||||
|
- fetches the current account state after opening;
|
||||||
|
- sends a heartbeat every 20 seconds;
|
||||||
|
- reconnects with exponential delays from 1 to 30 seconds.
|
||||||
|
|
||||||
|
## Validation and errors
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- unknown or unavailable variables;
|
||||||
|
- missing or unknown comparison operators;
|
||||||
|
- conditions split by whitespace;
|
||||||
|
- malformed ranges or multiple `-->` separators;
|
||||||
|
- missing range targets;
|
||||||
|
- negative target values or percentages;
|
||||||
|
- ranges whose lower target exceeds the upper target;
|
||||||
|
- missing action credentials or reserve settings;
|
||||||
|
- duplicate action IDs;
|
||||||
|
- invalid Pushover severities.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- `processed` commitment minimizes delay but an observed update may belong to a fork that is later abandoned.
|
||||||
|
- Solana PubSub is not a durable event log and does not guarantee delivery. Multiple independent endpoints reduce, but do not eliminate, this risk.
|
||||||
|
- Alarm state and grace-period timestamps are kept only in memory and reset on restart.
|
||||||
|
- Dynamic variables exist only for currently returned open positions.
|
||||||
|
- 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 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.
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
perl -0777 -pi -e 's/,\s+/,/g; s/\s+\z//' $1
|
||||||
+111
-6
@@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("java")
|
id("java")
|
||||||
|
id("application")
|
||||||
id("maven-publish")
|
id("maven-publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,6 +10,11 @@ if (version == "UNSET" && gradle.startParameter.taskNames.any { it.startsWith("p
|
|||||||
throw GradleException("You must set -Pversion=... (use publish.sh / publishCICD.sh)")
|
throw GradleException("You must set -Pversion=... (use publish.sh / publishCICD.sh)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("com.r35157.nenjim.hubd.Main")
|
||||||
|
applicationDefaultJvmArgs = listOf("--enable-preview")
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
|
||||||
@@ -27,18 +33,117 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val detag = configurations.create("detag") {
|
||||||
|
isCanBeConsumed = false
|
||||||
|
isCanBeResolved = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains:annotations:26.0.1")
|
detag("com.r35157.tools:detag-impl_ref:0.1.0")
|
||||||
implementation("log4j:log4j:1.2.17")
|
compileOnly("org.jetbrains:annotations:26.1.0")
|
||||||
implementation("com.r35157.nenjim:hubd-api:0.0.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")
|
||||||
|
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.6")
|
||||||
|
implementation("com.fazecast:jSerialComm:2.11.4")
|
||||||
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
|
implementation("commons-codec:commons-codec:1.22.0")
|
||||||
|
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 {
|
java {
|
||||||
toolchain { languageVersion.set(JavaLanguageVersion.of(24)) }
|
toolchain { languageVersion.set(JavaLanguageVersion.of(25)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
options.release.set(24)
|
options.release.set(25)
|
||||||
|
|
||||||
|
options.compilerArgs.addAll(
|
||||||
|
listOf(
|
||||||
|
"--enable-preview",
|
||||||
|
"-Xlint:deprecation",
|
||||||
|
"-Xlint:unchecked"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
|
||||||
|
|
||||||
|
val cleanGeneratedDetagMain = tasks.register<Delete>("cleanGeneratedDetagMain") {
|
||||||
|
delete(generatedDetagMain)
|
||||||
|
}
|
||||||
|
|
||||||
|
val detagMain = tasks.register<JavaExec>("detagMain") {
|
||||||
|
group = "build"
|
||||||
|
description = "Generates Java sources from .tjava files"
|
||||||
|
|
||||||
|
classpath = detag
|
||||||
|
mainClass.set("com.r35157.tools.detag.impl.ref.Main")
|
||||||
|
|
||||||
|
val configFile = layout.projectDirectory.file("../detag.conf")
|
||||||
|
val sourceRoot = layout.projectDirectory.dir("src/main/tjava")
|
||||||
|
|
||||||
|
inputs.file(configFile)
|
||||||
|
inputs.dir(sourceRoot)
|
||||||
|
outputs.dir(generatedDetagMain)
|
||||||
|
|
||||||
|
dependsOn(cleanGeneratedDetagMain)
|
||||||
|
|
||||||
|
args(
|
||||||
|
"--config", configFile.asFile.absolutePath,
|
||||||
|
"--source-root", sourceRoot.asFile.absolutePath,
|
||||||
|
"--out", generatedDetagMain.get().asFile.absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
// Human-written Detag source files. IntelliJ should treat this as a source root.
|
||||||
|
// Gradle's Java compiler will still only compile .java files directly from sourceSets,
|
||||||
|
// so the .tjava files are not compiled directly.
|
||||||
|
java.srcDir("src/main/tjava")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<JavaCompile>("compileJava") {
|
||||||
|
dependsOn(detagMain)
|
||||||
|
|
||||||
|
// Compiler input generated from src/main/tjava.
|
||||||
|
// Do not add this directory to sourceSets, or IntelliJ will see duplicate classes:
|
||||||
|
// MyClass.tjava + build/generated/.../MyClass.java.
|
||||||
|
source(generatedDetagMain)
|
||||||
|
}
|
||||||
|
|
||||||
|
val libsDir = layout.buildDirectory.dir("libs")
|
||||||
|
|
||||||
|
tasks.register<Sync>("prepareLibs") {
|
||||||
|
group = "distribution"
|
||||||
|
description = "Copies runtime deps to build/libs without deleting the app jar"
|
||||||
|
|
||||||
|
val jarTask = tasks.named<Jar>("jar")
|
||||||
|
dependsOn(jarTask)
|
||||||
|
|
||||||
|
into(libsDir)
|
||||||
|
|
||||||
|
// Kun deps (transitivt)
|
||||||
|
from(configurations.runtimeClasspath)
|
||||||
|
|
||||||
|
// Bevar jar-filen som jar-tasken allerede har lagt i build/libs
|
||||||
|
preserve {
|
||||||
|
include(jarTask.get().archiveFileName.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
export VERSION=$(git describe --tags --exact-match 2>/dev/null \
|
export VERSION=$(git describe --tags --exact-match 2>/dev/null \
|
||||||
|| git symbolic-ref --short -q HEAD \
|
|| git symbolic-ref --short -q HEAD \
|
||||||
|| git rev-parse --short HEAD)
|
|| git rev-parse --short HEAD)
|
||||||
|
|
||||||
echo "Building $VERSION..."
|
GITHASH=$(git rev-parse --short=8 HEAD)
|
||||||
./gradlew -Pversion=$VERSION jar
|
|
||||||
|
export VERSION_LONG=${VERSION}_${GITHASH}
|
||||||
|
|
||||||
|
echo "Building 'NenjimHub v${VERSION_LONG}'..."
|
||||||
|
|
||||||
|
./gradlew \
|
||||||
|
-Pversion=${VERSION} \
|
||||||
|
jar \
|
||||||
|
prepareLibs
|
||||||
@@ -1 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
./gradlew clean ; rm -rf build
|
./gradlew clean ; rm -rf build
|
||||||
|
|||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
source, destination = sys.argv[1], sys.argv[2]
|
||||||
|
|
||||||
|
with open(source) as f:
|
||||||
|
key = json.load(f)
|
||||||
|
|
||||||
|
if len(key) not in (32, 64):
|
||||||
|
raise SystemExit(f"Unexpected key length: {len(key)}")
|
||||||
|
|
||||||
|
if not all(isinstance(value, int) and 0 <= value <= 255 for value in key):
|
||||||
|
raise SystemExit("The file contains invalid byte values")
|
||||||
|
|
||||||
|
with open(destination, "w") as f:
|
||||||
|
f.write(json.dumps(key, separators=(",", ":")))
|
||||||
|
|
||||||
|
print(f"Created {destination} with {len(key)} bytes")
|
||||||
|
' $1 $2
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
# Alarm ids handled by ConsoleAlarmAction
|
||||||
|
##########################################
|
||||||
|
#1
|
||||||
|
#2
|
||||||
|
#3
|
||||||
|
#4
|
||||||
|
#5
|
||||||
|
#6
|
||||||
|
#7
|
||||||
|
#8
|
||||||
|
#9
|
||||||
|
#10
|
||||||
|
#11
|
||||||
|
#12
|
||||||
|
#13
|
||||||
|
#14
|
||||||
|
#15
|
||||||
|
#16
|
||||||
|
#17
|
||||||
|
#18
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FORMAT_VERSION=2
|
||||||
|
|
||||||
|
RESERVE_POSITION_SIZE_USD_LIMIT 10
|
||||||
|
|
||||||
|
# Id Asset Direction ReceiveToken SizeDeltaUSD MaxSlippageBPS
|
||||||
|
########################################################################
|
||||||
|
# Values may reference {{VARIABLES}} defined in alarms.conf.
|
||||||
|
# ReceiveToken must be USDC or match Asset.
|
||||||
|
#3 SOL SHORT USDC 5.00 200
|
||||||
|
#6 SOL LONG USDC 5.00 200
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FORMAT_VERSION=2
|
||||||
|
|
||||||
|
RESERVE_USDC_LIMIT 10
|
||||||
|
|
||||||
|
# Id Asset Direction CollateralUSDC SizeDeltaUSD MaxSlippageBPS
|
||||||
|
############################################################################
|
||||||
|
# Values may reference {{VARIABLES}} defined in alarms.conf.
|
||||||
|
#4 SOL LONG 0.25 2.50 200
|
||||||
|
#5 SOL LONG 0.25 6.25 200
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
# Pushover credentials
|
||||||
|
##########################################
|
||||||
|
# Values and notes may reference {{VARIABLES}} defined in alarms.conf.
|
||||||
|
APPLICATION_TOKEN <pushover-application-token>
|
||||||
|
USER_KEY <pushover-user-key>
|
||||||
|
|
||||||
|
|
||||||
|
# Id Severity Note
|
||||||
|
##########################################
|
||||||
|
|
||||||
|
# SOL SHORT
|
||||||
|
#1 CRITICAL "🚨 SOL Short add 1.1x"
|
||||||
|
#2 INFO "🌱 SOL Short add 3x"
|
||||||
|
#3 INFO "💵 SOL Short close"
|
||||||
|
|
||||||
|
# SOL LONG
|
||||||
|
#4 CRITICAL "🚨 SOL Long add 1.1x"
|
||||||
|
#5 INFO "🌱 SOL Long add 3x"
|
||||||
|
#6 INFO "💵 SOL Long close"
|
||||||
|
|
||||||
|
# ETH SHORT
|
||||||
|
#7 CRITICAL "🚨 ETH Short add 1.1x"
|
||||||
|
#8 INFO "🌱 ETH Short add 3x"
|
||||||
|
#9 INFO "💵 ETH Short close"
|
||||||
|
|
||||||
|
# ETH LONG
|
||||||
|
#10 CRITICAL "🚨 ETH Long add 1.1x"
|
||||||
|
#11 INFO "🌱 ETH Long add 3x"
|
||||||
|
#12 INFO "💵 ETH Long close"
|
||||||
|
|
||||||
|
# BTC SHORT
|
||||||
|
#13 CRITICAL "🚨 BTC Short add 1.1x"
|
||||||
|
#14 INFO "🌱 BTC Short add 3x"
|
||||||
|
#15 INFO "💵 BTC Short close"
|
||||||
|
|
||||||
|
# BTC LONG
|
||||||
|
#16 CRITICAL "🚨 BTC Long add 1.1x"
|
||||||
|
#17 INFO "🌱 BTC Long add 3x"
|
||||||
|
#18 INFO "💵 BTC Long close"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
FORMAT_VERSION=2
|
||||||
|
|
||||||
|
# Constant Name Value
|
||||||
|
#####################################
|
||||||
|
|
||||||
|
#{{SOL_LONG_LIQ_PRICE}} 74.69
|
||||||
|
#{{SOL_SHORT_LIQ_PRICE}} 76.77
|
||||||
|
|
||||||
|
#{{ETH_LONG_LIQ_PRICE}} 1736.32
|
||||||
|
#{{ETH_SHORT_LIQ_PRICE}} 1768.08
|
||||||
|
|
||||||
|
#{{BTC_LONG_LIQ_PRICE}} 62158.45
|
||||||
|
#{{BTC_SHORT_LIQ_PRICE}} 63275.97
|
||||||
|
|
||||||
|
|
||||||
|
# Id Asset Condition Trigger
|
||||||
|
################################################################################################
|
||||||
|
|
||||||
|
# SOL SHORT
|
||||||
|
#1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
|
#2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
|
#3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# SOL LONG
|
||||||
|
#4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
|
#5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
|
#6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# ETH SHORT
|
||||||
|
#7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
|
#8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
|
#9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# ETH LONG
|
||||||
|
#10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
|
#11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
|
#12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# BTC SHORT
|
||||||
|
#13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
|
#14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
|
#15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# BTC LONG
|
||||||
|
#16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
|
#17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
|
#18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|
||||||
|
# CROSSING with hysteresis
|
||||||
|
# Triggers above the entry price and re-arms at or below entry price minus 0.25%
|
||||||
|
#19 SOL >{{SOL_LONG_ENTRY_PRICE}}/0.25% CROSSING
|
||||||
@@ -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,20 @@
|
|||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
# Iteration interval in seconds
|
||||||
|
3600
|
||||||
|
|
||||||
|
# SYRUP owned by Evelyn
|
||||||
|
# = 1270,19474 + 85,22 + 27,00540296 + 256,873447 + 83,7839
|
||||||
|
1723.07749
|
||||||
|
|
||||||
|
# Solana address for Evelyn IOU
|
||||||
|
vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
|
||||||
|
|
||||||
|
# Solana address for Evelyn IOU Burner
|
||||||
|
5kjVTc1rebYHg56NyTxSSN1XMKqT2E81eWiaSx1CmzmV
|
||||||
|
|
||||||
|
# Raydium Pool Id
|
||||||
|
CndJzCXspuk39cwGAA43h3EtHJeHVZ81syt5bTh9dG3E
|
||||||
|
|
||||||
|
# Curve width in percent in both directions
|
||||||
|
0.05
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<RollingFile name="File"
|
||||||
|
fileName="logs/NenjimHub.log"
|
||||||
|
filePattern="logs/NenjimHub-%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
|
<PatternLayout pattern="%d{ISO8601} %-5p %c - %m%n"/>
|
||||||
|
|
||||||
|
<Policies>
|
||||||
|
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||||
|
<SizeBasedTriggeringPolicy size="50 MB"/>
|
||||||
|
</Policies>
|
||||||
|
|
||||||
|
<DefaultRolloverStrategy max="14"/>
|
||||||
|
</RollingFile>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Root level="info">
|
||||||
|
<AppenderRef ref="File"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
FORMAT_VERSION=1
|
||||||
|
|
||||||
|
# MintA = Solana
|
||||||
|
# MintB = SyrupUSDC
|
||||||
|
#
|
||||||
|
# NFTId ; AddedMintA ; AddedMintB ; BorrowedMintA ; BorrowedMintB
|
||||||
|
89LikiLxkj1m9dU2SVLnzj2Dky3qrFNUbwZYyrVNSzpt;0;5;0;0 # 63.681428 - 64.064646
|
||||||
|
X7bKgeVRaRuM2QrkcxGbGiuzz9YzR25mxpg43CbLLPB;0;5;0;0 # 64.064646 - 64.450170
|
||||||
@@ -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
|
||||||
+23
@@ -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
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
jars=(libs/*.jar)
|
||||||
|
if (( ${#jars[@]} == 0 )); then
|
||||||
|
echo "ERROR: No JARs found in libs/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLASSPATH=$(IFS=:; echo "${jars[*]}")
|
||||||
|
|
||||||
|
DEBUG_PORT="${DEBUG_PORT:-5005}"
|
||||||
|
DEBUG_SUSPEND="${DEBUG_SUSPEND:-y}"
|
||||||
|
|
||||||
|
echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})"
|
||||||
|
|
||||||
|
exec java \
|
||||||
|
--enable-preview \
|
||||||
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
|
||||||
|
-Dlog4j.configurationFile=conf/log4j2.xml \
|
||||||
|
-cp "$CLASSPATH" \
|
||||||
|
com.r35157.nenjim.hubd.Main
|
||||||
@@ -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
@@ -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
@@ -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.
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<!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>, and the first central component Registry 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><GROUP>-<MODULE>-<ARTIFACT>
|
||||||
|
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-->1.0.7]</code></td><td>A closed range including both exact endpoints.</td></tr>
|
||||||
|
<tr><td><code>[1.0-->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--></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-->
|
||||||
|
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. 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 active Mission
|
||||||
|
Control and Process Manager. All six modern JavaFX implementations require that service in their constructor.
|
||||||
|
Only service startup calls <code>Platform.startup(...)</code>; task submission never initializes or retries
|
||||||
|
JavaFX and is rejected until startup succeeds. Startup waits until the JavaFX Application Thread is ready and
|
||||||
|
sets <code>Platform.setImplicitExit(false)</code>, keeping the shared runtime alive after the last window
|
||||||
|
closes.</p>
|
||||||
|
<pre><code>data/nenjim/journals/
|
||||||
|
└── <artifact-coordinate>/
|
||||||
|
├── <journal-version>.journal
|
||||||
|
└── <journal-version>.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>
|
||||||
|
<p><code>NenjimHubImpl</code> is the global composition and runtime owner. It constructs the Registry assembly,
|
||||||
|
starts and registers Registry first, and records its successful process ID. It then constructs the Journal
|
||||||
|
assembly for <code>data/nenjim/journals</code>, registers <code>nenjim.journal.service</code>, starts Journal
|
||||||
|
second, and records it only after complete successful loading. Only then does the Hub construct the hardcoded
|
||||||
|
business component graph in dependency order. A Journal failure therefore stops startup before Ticker,
|
||||||
|
Evelyn, Burner, the JavaFX runtime, Mission Control, Process Manager, the online point, and the blocking
|
||||||
|
wait.</p>
|
||||||
|
<p>The Ticker explicitly selects the Raydium EVE/USDT source ID while the hardcoded source remains registered but
|
||||||
|
inactive. After Registry and Journal, the Hub activates the explicit Ticker, Production Evelyn, Test Evelyn,
|
||||||
|
Production IOU Burner, JavaFX runtime service, Mission Control, and Process Manager sequence; it does not
|
||||||
|
start every registered process. The shared JavaFX runtime is therefore active before either enabled JavaFX
|
||||||
|
application. Successful process IDs, including <code>nenjim.javafx-runtime.service</code>, are exposed as
|
||||||
|
immutable ordered point-in-time snapshots without implying continued health. A JavaFX startup failure is
|
||||||
|
fail-fast and prevents Mission Control and Process Manager from starting.</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, or automatic
|
||||||
|
selection.</p>
|
||||||
|
|
||||||
|
<h2>Future Nenjim vision</h2>
|
||||||
|
<p>Contexts, dependency-graph resolution, 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 implemented
|
||||||
|
Journal foundation.</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+445
@@ -0,0 +1,445 @@
|
|||||||
|
#<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` og første version af det centrale component Registry
|
||||||
|
> 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 nuværende hardcodede
|
||||||
|
componenter. 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.
|
||||||
|
|
||||||
|
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 konstruerer Registry- og Journal-subsystemerne samt
|
||||||
|
hele den nuværende hardcodede component graph, ejer dependency wiring og vælger de processer, som skal startes. 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. Hub'en konstruerer og registrerer objektet som `nenjim.javafx-runtime.service`, starter det før de aktive
|
||||||
|
JavaFX-applikationer og injicerer service-viewet i dem gennem deres constructors. 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)`, så den delte
|
||||||
|
runtime forbliver aktiv, når det sidste vindue lukkes.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Applikationen ejer sine konkrete valg og sin activation. Tickerens initiale configuration vælger eksplicit
|
||||||
|
`assetaz.price-source.raydium-pool.eve-usdt` gennem Registry'et; den hardcodede price source er registreret, men
|
||||||
|
inaktiv. Hub'en starter Registry-processen først og registrerer den som `nenjim.registry.service`. Journal-servicen
|
||||||
|
registreres dernæst som `nenjim.journal.service`, startes som den anden proces og skal være fuldt initialiseret, før
|
||||||
|
business graph'en fortsætter. Derefter starter Hub'en kun den nuværende eksplicitte, dependency-safe liste: Ticker,
|
||||||
|
Production Evelyn, Test Evelyn, Production IOU Burner, JavaFX-runtime-servicen, Mission Control og Process Manager.
|
||||||
|
JavaFX-runtime-servicen startes altså før begge aktive JavaFX-applikationer; Composer, Test Tool, Soda Task Manager
|
||||||
|
og Suwimo Client er fortsat inaktive, men bruger samme constructor-injection model. Alarmen bliver heller ikke
|
||||||
|
automatisk aktiveret. Efter hvert succesfuldt `start()` registrerer Hub'en process-ID'et i et immutable, ordered
|
||||||
|
snapshot, inklusive `nenjim.javafx-runtime.service`; et fejlet start bliver ikke registreret, og listen lover ikke
|
||||||
|
fortsat health eller liveness. JavaFX-startfejl stopper opstarten før Mission Control og Process Manager. Startup er
|
||||||
|
fail-fast og kan kun forsøges én gang uden generic rollback.
|
||||||
|
|
||||||
|
`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 selection/start-all, usage tracking og permissions er fremtidigt arbejde. 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-<ditid>'
|
||||||
|
(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
|
||||||
|
```
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#<center><H1>Soda Task Manager<br>Documentation</H1></center>
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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 og første version af det centrale
|
||||||
|
> component Registry 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. |
|
||||||
|
| NenjimHub | Den globale ejer af én kørende Nenjim runtime. Hub'en konstruerer og registrerer component graph'en, ejer dependency wiring og startup-policy, tracker succesfuldt startede process-ID'er, starter den fælles JavaFX-runtime før dens applikationer, erklærer online point og ejer den afsluttende runtime wait. Den er hverken component eller process og registreres ikke i sit eget Registry. |
|
||||||
|
| 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. Hub'en ejer dets eneste startup-sti gennem `JavaFxRuntimeServiceImpl` og holder det aktivt efter sidste lukkede vindue med `Platform.setImplicitExit(false)`. |
|
||||||
|
| 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. |
|
||||||
|
| 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). |
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
org.gradle.java.installations.auto-detect=true
|
org.gradle.java.installations.auto-detect=true
|
||||||
org.gradle.java.installations.fromEnv=JAVA_HOME
|
org.gradle.java.installations.fromEnv=JAVA_HOME
|
||||||
org.gradle.java.installations.paths=/usr/local/software/java/jfx-24
|
org.gradle.java.installations.paths=/usr/local/software/java/jfx-25
|
||||||
org.gradle.java.installations.auto-download=false
|
org.gradle.java.installations.auto-download=false
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -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.
|
||||||
+23
@@ -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.
|
||||||
+69
@@ -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.
|
||||||
+2
@@ -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.
|
||||||
+122
@@ -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.
|
||||||
+112
@@ -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.
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-08
|
||||||
+78
@@ -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.
|
||||||
+24
@@ -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.
|
||||||
+17
@@ -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
|
||||||
|
|
||||||
+76
@@ -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
|
||||||
|
|
||||||
+27
@@ -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.
|
||||||
+2
@@ -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.
|
||||||
+71
@@ -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.
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-08
|
||||||
+54
@@ -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.
|
||||||
+26
@@ -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.
|
||||||
+122
@@ -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
|
||||||
+23
@@ -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.
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-10
|
||||||
+65
@@ -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.
|
||||||
+26
@@ -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.
|
||||||
+94
@@ -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
|
||||||
+25
@@ -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.
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
+62
@@ -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.
|
||||||
+26
@@ -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.
|
||||||
+96
@@ -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.
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
+64
@@ -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.
|
||||||
+27
@@ -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.
|
||||||
+68
@@ -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
|
||||||
+44
@@ -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
|
||||||
+22
@@ -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.
|
||||||
+72
@@ -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.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user