@btusbefw/dkg-experiment-ledger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEMO.md +11 -0
- package/DESIGN.md +57 -0
- package/LAB.md +43 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/SECURITY.md +20 -0
- package/bin/cli.mjs +35 -0
- package/demo/transcript.txt +77 -0
- package/demo/walkthrough.cast +78 -0
- package/examples/autoresearch.mjs +38 -0
- package/examples/notebook.py +31 -0
- package/package.json +44 -0
- package/scripts/demo-session.mjs +37 -0
- package/src/ledger.mjs +100 -0
- package/src/record.mjs +108 -0
- package/src/token.mjs +11 -0
package/DEMO.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Recorded walkthrough
|
|
2
|
+
|
|
3
|
+
`demo/walkthrough.cast` is an actual recorded terminal session (asciinema v2), not invented terminal output. `demo/transcript.txt` is its readable transcript. Play the recording with `asciinema play demo/walkthrough.cast`, or upload it using the new dedicated project identity when preparing the public submission.
|
|
4
|
+
|
|
5
|
+
The walkthrough runs `scripts/demo-session.mjs` against a real DKG 10.0.16 node. It records two measured regression results, detects a repeated record, reads and compares the stored runs, explicitly shares the candidate with Curator authority, and queries its self-attested data from Shared Memory. Two-second pauses make the output easier to read.
|
|
6
|
+
|
|
7
|
+
The example's deterministic held-out mean squared errors are approximately 0.7939276 (degree one) and 0.00495805 (degree three). These are measurements on the included synthetic example, not a claim of real-world model improvement. The dataset, source, parameters, and record digests are preserved.
|
|
8
|
+
|
|
9
|
+
The node is isolated with no external peers and a mock chain. The recording proves local API functionality and sealed sharing, not public replication, mainnet publication, or payment. The public hosted demo URL is pending the dedicated account setup.
|
|
10
|
+
|
|
11
|
+
To reproduce: configure a local registered-agent token and an existing test Project, run `node examples/autoresearch.mjs --offline`, then run `node scripts/demo-session.mjs`. Use a fresh disposable Project if you want the first writes shown as new records rather than idempotent replays. Do not use a production project for this demo: the script deliberately performs SHARE.
|
package/DESIGN.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Design brief: reproducible experiment memory
|
|
2
|
+
|
|
3
|
+
## Problem and target user
|
|
4
|
+
|
|
5
|
+
An autoresearch loop can lose the reason an experiment was kept or rejected when its session ends. A notebook cell's printed metric alone does not identify the evaluated data, source, settings, or author. DKG Experiment Ledger gives coding/research agents and Jupyter kernels a small explicit record boundary, preserving those relationships in a project-scoped knowledge graph.
|
|
6
|
+
|
|
7
|
+
This addresses the Round 1 call's named autoresearch and Jupyter/notebook integration targets. The end-to-end example actually trains two regressions, stores their measured held-out results, retrieves them from DKG, and compares compatible metrics. The integration can be used without a hosted LLM, framework plugin, paid model API, or a second persistence service.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Autoresearch agent / Jupyter kernel
|
|
13
|
+
| explicit JSON result + source/data fingerprints
|
|
14
|
+
v
|
|
15
|
+
Validate -> canonical digest -> deterministic Knowledge Asset name
|
|
16
|
+
| public authenticated HTTP
|
|
17
|
+
v
|
|
18
|
+
Project / Context Graph -> private Working Memory
|
|
19
|
+
| read-back and content-digest check
|
|
20
|
+
+-> scoped SPARQL recall -> compatible-run comparison
|
|
21
|
+
|
|
|
22
|
+
+-> explicit Curator-authorized SHARE -> Shared Memory
|
|
23
|
+
|
|
|
24
|
+
official agent PUBLISH conversation
|
|
25
|
+
v
|
|
26
|
+
Verifiable Memory / actual UAL -> oracle
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The CLI is the primary agent surface. The Python helper accepts an explicit record from an existing kernel; it does not execute or scrape a notebook. The JavaScript library exposes the same operations. These surfaces all call the public HTTP API; no internal DKG imports, node patches, or direct SPARQL updates are used.
|
|
30
|
+
|
|
31
|
+
## Data model and trust
|
|
32
|
+
|
|
33
|
+
Each run is a named Knowledge Asset whose root Entity is a `urn:sha256:` content identifier. Standard PROV predicates associate attribution and observation time; integration predicates expose the experiment, dataset fingerprint, source revision, typed numeric metrics, and a canonical JSON payload. The payload is authoritative for comparisons. Its SHA-256 detects corruption relative to the deterministic name; it does not prove the author's measurements are true or certify unrelated RDF statements.
|
|
34
|
+
|
|
35
|
+
Private drafts remain self-attested. The experiment and evaluation-data fingerprint must match before comparing runs; metric units and optimization direction must also agree. Parameter/source changes are exposed, and the output expressly avoids statistical/causal claims. Dataset fingerprints should cover evaluation splits and preprocessing. No UAL is synthesized before publication.
|
|
36
|
+
|
|
37
|
+
## DKG v10 primitives and authority
|
|
38
|
+
|
|
39
|
+
Projects scope every operation via the exact Context Graph ID. Records use the Knowledge Asset create/write lifecycle, and read-back verifies the actual stored payload. The Curator retains control of SHARE through DKG's authorization path. An explicit exact-project confirmation protects the privacy transition. No public-project fallback, privilege substitution, or `skipSeal` downgrade is implemented.
|
|
40
|
+
|
|
41
|
+
The registry install kind is `cli`, public interface `http-api`, and memory layers `WM`/`SWM`. The product calls it Shared Memory as in the bounty; the node's API view literal remains `shared-working-memory`. The CLI creates no Context Graphs; the operator selects an existing Project. Test setup creates disposable Context Graphs outside the shipped operational surface.
|
|
42
|
+
|
|
43
|
+
## Promotion path and oracle readiness
|
|
44
|
+
|
|
45
|
+
The author or Curator reviews the source hash, evaluation fingerprint, declared artifacts, and reproducibility evidence in Working Memory. Authorized SHARE invokes the node's full sealing path, preserving the Entity, assertion lineage, metrics, and provenance into Shared Memory. When the team decides an experiment warrants permanence, the official DKG agent conversation handles PUBLISH and its fees. The resulting real UAL can be paired with the content digest and source/data hashes in an oracle input.
|
|
46
|
+
|
|
47
|
+
Sealing is an integrity/authorship step, not independent verification of a metric. Endorsement and consensus verification remain conversational DKG operations, not UI buttons. This integration does not automatically spend TRAC, endorse results, or declare self-attested data consensus-verified.
|
|
48
|
+
|
|
49
|
+
## Reliability, validation, and limits
|
|
50
|
+
|
|
51
|
+
Validation is bounded to 128 KiB and rejects non-finite metrics, unsafe field shapes, incompatible comparisons, and credential-bearing URLs. Read-after-write catches storage/format errors. Completed replays are idempotent even where the tested DKG runtime's `alreadyExists` flag misses an agent-specific record. Partial writes require reconciliation of the deterministic name; mutations are never blindly retried.
|
|
52
|
+
|
|
53
|
+
Tests use DKG 10.0.16, Oxigraph, registered local-agent tokens, and a mock chain. They cover real storage/query/SHARE, not a mocked HTTP service alone. No public-network replication, blockchain settlement, or multi-node confidentiality claim is made. The current command surface is bounded to 100 listed runs (up to 1,000 through the library); pagination, notebook UI, and remote artifact verification are future work.
|
|
54
|
+
|
|
55
|
+
## Maintenance
|
|
56
|
+
|
|
57
|
+
The release is dependency-free, with unit tests and an isolated real-node suite that can detect lifecycle changes. The submitting operator must name the dedicated maintainer and accept the required six-month support window. This draft does not invent that identity or attest on their behalf.
|
package/LAB.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Isolated DKG validation lab
|
|
2
|
+
|
|
3
|
+
Tested 2026-09-09 with the published `@origintrail-official/dkg@10.0.16`, reported source commit `ef7e7400ff1571fd31cc0c39110fa335949f444d`, Node.js 25.6.0, macOS arm64. The library requires Node 22+. The node uses Oxigraph plus its own SQLite metadata store.
|
|
4
|
+
|
|
5
|
+
The test install lives under `/tmp/astra-dkg-lab`, with a separate `DKG_HOME=/tmp/astra-dkg-lab/state`. It does not use an existing node, personal wallet, browser account, npm login, GitHub credential, or mainnet funds. Installation used empty npm user/global configs and a temporary cache. Lifecycle scripts were initially disabled; only the required `better-sqlite3` native dependency was subsequently rebuilt. No DKG source was patched.
|
|
6
|
+
|
|
7
|
+
The lab config selects the bundled testnet genesis but overrides the chain adapter to `mock:31337`, disables relays (`relay: "none"`), sets an empty bootstrap list, and disables telemetry. The API binds `127.0.0.1:19200`. On macOS the process additionally runs through `sandbox-exec` with non-loopback network traffic denied. This blocks incidental upstream node update checks as well as public peer/RPC traffic. Status reported zero connected peers.
|
|
8
|
+
|
|
9
|
+
Example config (new disposable directory only):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"name": "astra-experiment-lab",
|
|
14
|
+
"networkConfig": "testnet",
|
|
15
|
+
"apiPort": 19200,
|
|
16
|
+
"apiHost": "127.0.0.1",
|
|
17
|
+
"listenPort": 0,
|
|
18
|
+
"nodeRole": "edge",
|
|
19
|
+
"relay": "none",
|
|
20
|
+
"bootstrapPeers": [],
|
|
21
|
+
"chain": {"type": "mock", "chainId": "mock:31337"},
|
|
22
|
+
"store": {"backend": "oxigraph-persistent", "options": {"path": "/tmp/astra-dkg-lab/state/store.nq"}},
|
|
23
|
+
"autoUpdate": {"enabled": false},
|
|
24
|
+
"telemetry": {"enabled": false},
|
|
25
|
+
"contextGraphs": [],
|
|
26
|
+
"contextGraphSubscriptionRehydrationEnabled": false,
|
|
27
|
+
"rfc64Catalog": {"enabled": false}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Start the official CLI with `DKG_HOME` set to that directory. Register a **disposable local agent** through `POST /api/agent/register`, store the returned token privately, and use it for tests. Never print the returned private key/token or include the runtime state in a release. The regular node-admin token without a wallet created legacy draft graphs that the version's broad WM query did not discover; the registered-agent path passes.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
export DKG_URL=http://127.0.0.1:19200
|
|
35
|
+
export DKG_TOKEN_FILE=/tmp/astra-dkg-lab/state/ledger.token
|
|
36
|
+
DKG_TEST_LOCAL=1 npm run test:integration
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both `backend: oxigraph-persistent` and `store.options.path` are essential: `backend: oxigraph` is in-memory and ignores a path option. Restart checks caught that lab misconfiguration; durable checks use the persistent adapter and file path above. The test creates uniquely named disposable Projects. It refuses nodes with a different lab name, a non-loopback URL, or connected peers. Delete only the disposable lab after recording evidence. The tests do not validate public-network gossip, blockchain settlement, an exchange off-ramp, or sponsor payment.
|
|
40
|
+
|
|
41
|
+
Runtime-specific observations are captured in regression behavior: generated auth.token files include comment lines; query bindings are nested under `result.bindings`; `alreadyExists` can miss custodial records. The integration handles the first two and uses content read-back instead of trusting the third.
|
|
42
|
+
|
|
43
|
+
Final persistence check passed: the recorded asset and digest were read using the same registered-agent token after stopping and restarting the persistent node. Evidence is retained in the parent execution directory.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DKG Experiment Ledger contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# DKG Experiment Ledger
|
|
2
|
+
|
|
3
|
+
Record reproducible experiment results in a DKG v10 Project, then retrieve and compare them from an autoresearch agent or Jupyter kernel. The integration uses the node's public HTTP API. It has zero runtime dependencies and never executes the recorded command or reads your notebook history.
|
|
4
|
+
|
|
5
|
+
## Run locally
|
|
6
|
+
|
|
7
|
+
Requires Node.js 22+ and a running DKG v10 node (tested on 10.0.16). Use a registered agent's token and an exact Context Graph ID that this agent may access. A bare node-admin identity without an EVM agent wallet has incomplete Working Memory discovery on the tested DKG version; register a local agent instead.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
export DKG_URL=http://127.0.0.1:9200
|
|
11
|
+
export DKG_TOKEN_FILE=/path/to/your/local-agent.token
|
|
12
|
+
export DKG_CONTEXT_GRAPH=your-exact-project-id
|
|
13
|
+
node examples/autoresearch.mjs --offline
|
|
14
|
+
node bin/cli.mjs validate demo-output/degree-1.json
|
|
15
|
+
node bin/cli.mjs record demo-output/degree-1.json
|
|
16
|
+
node bin/cli.mjs list
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The offline demo really fits degree-one and degree-three regressions to deterministic synthetic training data and measures held-out mean squared error. Running without `--offline` writes both results to DKG and compares records retrieved from the node. The generated records contain source and dataset SHA-256 fingerprints; `demo-output/receipt.json` records the verified result. This deliberately small numerical example demonstrates the integration, not a claim about general machine-learning performance.
|
|
20
|
+
|
|
21
|
+
The release package is `@btusbefw/dkg-experiment-ledger` (publication pending).
|
|
22
|
+
|
|
23
|
+
After publishing/installing the package, `dkg-experiments` replaces `node bin/cli.mjs`. `DKG_API_URL` and `DKG_AUTH_TOKEN` are accepted aliases for registry-provided configuration. Token files may contain DKG's comment header. No token value is printed. HTTPS remote nodes require explicit `DKG_ALLOW_REMOTE=1`; otherwise only loopback destinations are allowed.
|
|
24
|
+
|
|
25
|
+
## Record contract
|
|
26
|
+
|
|
27
|
+
See the generated `demo-output/degree-1.json` for a complete example. Required fields:
|
|
28
|
+
|
|
29
|
+
- `schemaVersion: 1`, `experiment`, `runId`, `observedAt` (UTC ISO), `author` (your agent identifier).
|
|
30
|
+
- `dataset: {name, fingerprint}`. The SHA-256 fingerprint should identify the effective evaluation data, split, and preprocessing protocol; use the same convention throughout an experiment.
|
|
31
|
+
- `code: {revision}`: a full Git SHA or source SHA-256. Optional `repository` must be a clean HTTPS URL.
|
|
32
|
+
- `parameters`: JSON object. `metrics`: named `{value, unit, direction}` entries; direction is `minimize` or `maximize`.
|
|
33
|
+
- Optional `command` (an argument array, stored only), `notes`, and `artifacts` (name, SHA-256, optional media type and clean HTTPS URI).
|
|
34
|
+
|
|
35
|
+
Only explicitly supplied data is stored. Do not include secrets or personal data in notes, parameters, commands, or artifacts. URL checks reject query strings and embedded credentials, but are not a general secret detector. Artifact hashes are declared provenance; the integration does not fetch or independently verify external artifacts.
|
|
36
|
+
|
|
37
|
+
## Recall, compare, and promote
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
dkg-experiments show xrun-<48-hex-name>
|
|
41
|
+
dkg-experiments compare xrun-<baseline> xrun-<candidate>
|
|
42
|
+
dkg-experiments promotion-plan xrun-<candidate>
|
|
43
|
+
dkg-experiments share xrun-<candidate> your-exact-project-id
|
|
44
|
+
dkg-experiments list shared-working-memory
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`record` writes private Working Memory. Names derive from the canonical record digest, and repeating a completed record is detected through read-back before mutation. A partial/ambiguous write never causes a random replacement asset or a blind write retry; an existing lifecycle without readable payload fails with a recovery message.
|
|
48
|
+
|
|
49
|
+
Comparisons require the same experiment and dataset fingerprint, and matching metric units/directions. They expose parameter changes and source revisions, refuse incompatible metrics, and avoid dividing by zero. Differences are observations, not causal effects or statistical significance. `list` returns at most 100 records by default (library callers can request up to 1,000); it is not a full unlimited history export.
|
|
50
|
+
|
|
51
|
+
`share` requires the exact Project ID again, calls the canonical Curator-authorized SHARE route, and lets DKG seal the asset. It never bypasses denial or retries with `skipSeal`. Shared Memory remains self-attested. On-chain PUBLISH is deliberately left to the official DKG agent conversation and the operator's fee authorization. Keep the actual returned UAL for oracle citations; a content hash or a Working Memory name is not a UAL. Shared records are retrieved through `list shared-working-memory`; `show` and `compare` currently read Working Memory.
|
|
52
|
+
|
|
53
|
+
## Notebook integration
|
|
54
|
+
|
|
55
|
+
Import `record_experiment` from `examples/notebook.py` into a Jupyter kernel or ordinary Python program:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
receipt = record_experiment(my_explicit_record,
|
|
59
|
+
cli=["node", "/absolute/path/to/bin/cli.mjs"])
|
|
60
|
+
print(receipt["name"], receipt["digest"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The bridge forwards only DKG configuration and minimal runtime variables. It uses a private temporary JSON file and a shell-free subprocess call. It does not discover credentials or inspect user accounts.
|
|
64
|
+
|
|
65
|
+
## Validation
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm test
|
|
69
|
+
# Only against the isolated lab node described in LAB.md:
|
|
70
|
+
DKG_TEST_LOCAL=1 npm run test:integration
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The real-node suite proves round-trip storage, duplicate detection, project isolation, bad-token denial, comparison, and authorized sealed sharing. `LAB.md` describes the test node and its limitations. See `DESIGN.md`, `SECURITY.md`, and `DEMO.md` for the bounty review materials.
|
|
74
|
+
|
|
75
|
+
Maintainer identity, publication provenance, and the six-month maintenance commitment must be completed by the submitting operator before registry submission.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Security and data handling
|
|
2
|
+
|
|
3
|
+
- Default egress: only the configured loopback DKG node. No telemetry, analytics, wallet calls, model API calls, or third-party artifact downloads.
|
|
4
|
+
- Optional remote mode: `DKG_ALLOW_REMOTE=1` permits an explicitly configured HTTPS DKG host. That host receives the supplied records and token. Declare the actual host if distributing a preconfigured remote deployment. HTTP redirects are rejected to prevent credential forwarding.
|
|
5
|
+
- Credentials: one caller-supplied local DKG token (file or environment); registry aliases are supported. No discovery of GitHub, Google, npm, browser, wallet, or personal-account credentials. The token is never included in record payloads or errors generated from server response bodies.
|
|
6
|
+
- Write authority: `POST /api/knowledge-assets`, `POST /api/knowledge-assets/{name}/wm/write`, and explicit `POST /api/knowledge-assets/{name}/swm/share`. SHARE is a Curator-authority operation and requires exact Context Graph confirmation. DKG remains the authorization boundary.
|
|
7
|
+
- Read-only POST: `POST /api/query`, always scoped to the selected Context Graph and memory view. No direct SPARQL updates.
|
|
8
|
+
- No PUBLISH, endorsement, voting, token approval, staking, payment, or wallet-signing code is present.
|
|
9
|
+
- No install scripts, dynamic code loading, shell evaluation, or remote-code execution. Recorded commands are data. The Python helper uses a fixed argument array and a private temporary file.
|
|
10
|
+
- Records may contain sensitive information supplied by the caller. This is not a DLP/secret-scanning product. Read the record before sharing it; artifact URLs reject embedded credentials and query strings but free-text fields cannot be certified secret-free.
|
|
11
|
+
- Digest verification covers the canonical record payload and its root Entity/name. It is not a proof of experimental truth, external artifact availability, or integrity of all RDF statements a separate actor might add.
|
|
12
|
+
- A caller's deliberate `DKG_AGENT_ADDRESS` override must refer to an identity they are authorized to read. DKG enforces access; the integration does not bypass a denial.
|
|
13
|
+
|
|
14
|
+
## Recovery
|
|
15
|
+
|
|
16
|
+
On network timeout, inspect the same deterministic Knowledge Asset name. Repeating `record` reads a completed matching payload before mutation. If a lifecycle exists but its Working Memory payload is absent or incomplete, the operation fails; inspect the node's lifecycle and repair deliberately. Do not delete a shared/published asset just to retry.
|
|
17
|
+
|
|
18
|
+
## Test boundary
|
|
19
|
+
|
|
20
|
+
The isolated lab has no external peers, uses a mock chain, and runs under an OS profile that blocks non-loopback traffic. The integration itself uses public HTTP only; the lab uses the official published node with no source patches. Keys/tokens generated by the disposable node are not release artifacts.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {readFile} from 'node:fs/promises';
|
|
3
|
+
import {ExperimentLedger,encodeRecord,compareRecords} from '../src/ledger.mjs';
|
|
4
|
+
import {loadToken} from '../src/token.mjs';
|
|
5
|
+
|
|
6
|
+
const usage=`dkg-experiments <command> [arguments]
|
|
7
|
+
validate <record.json> Validate and show the deterministic KA name; no network
|
|
8
|
+
record <record.json> Write to the configured project's private Working Memory
|
|
9
|
+
show <KA-name> Read and verify a record
|
|
10
|
+
list [working-memory|shared-working-memory] Retrieve up to 100 project records
|
|
11
|
+
compare <baseline-KA> <candidate-KA> Compare compatible runs in Working Memory
|
|
12
|
+
share <KA-name> <exact-CG-ID> Explicit Curator-authorized sharing, sealed by DKG
|
|
13
|
+
promotion-plan <KA-name> Describe the promotion path; never publishes
|
|
14
|
+
Environment: DKG_URL, DKG_TOKEN or DKG_TOKEN_FILE, DKG_CONTEXT_GRAPH,
|
|
15
|
+
optional DKG_AGENT_ADDRESS and DKG_ALLOW_REMOTE=1 (HTTPS only).
|
|
16
|
+
No command creates a project, executes a notebook, or publishes on-chain.`;
|
|
17
|
+
try {
|
|
18
|
+
const [command,...args]=process.argv.slice(2);
|
|
19
|
+
const counts={validate:1,record:1,show:1,list:0,compare:2,share:2,'promotion-plan':1};
|
|
20
|
+
if(!command||command==='--help'){console.log(usage);process.exit(0);}
|
|
21
|
+
if(!(command in counts)||args.length<counts[command]||args.length>(command==='list'?1:counts[command])) throw new Error(usage);
|
|
22
|
+
let result;
|
|
23
|
+
if(command==='validate') {const e=encodeRecord(JSON.parse(await readFile(args[0],'utf8')));result={valid:true,name:e.name,digest:e.digest,quadCount:e.quads.length};}
|
|
24
|
+
else {
|
|
25
|
+
const token=await loadToken();
|
|
26
|
+
const ledger=new ExperimentLedger({baseUrl:process.env.DKG_URL??process.env.DKG_API_URL,token,contextGraphId:process.env.DKG_CONTEXT_GRAPH,agentAddress:process.env.DKG_AGENT_ADDRESS,allowRemote:process.env.DKG_ALLOW_REMOTE==='1'});
|
|
27
|
+
if(command==='record')result=await ledger.record(JSON.parse(await readFile(args[0],'utf8')));
|
|
28
|
+
if(command==='show')result=await ledger.get(args[0]);
|
|
29
|
+
if(command==='list')result=await ledger.list({view:args[0]});
|
|
30
|
+
if(command==='compare')result=compareRecords((await ledger.get(args[0])).record,(await ledger.get(args[1])).record);
|
|
31
|
+
if(command==='share')result=await ledger.share(args[0],{confirmContextGraphId:args[1]});
|
|
32
|
+
if(command==='promotion-plan')result=ledger.promotionPlan(args[0]);
|
|
33
|
+
}
|
|
34
|
+
console.log(JSON.stringify(result,null,2));
|
|
35
|
+
} catch(error){console.error(error.message);process.exitCode=1;}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
DKG EXPERIMENT LEDGER | live local API walkthrough
|
|
2
|
+
{
|
|
3
|
+
"nodeVersion": "10.0.16",
|
|
4
|
+
"project": "astra-recorded-demo-final",
|
|
5
|
+
"connectedPeers": 0,
|
|
6
|
+
"chain": "Isolated lab mock chain; no public publication"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
1. Record two measured regression runs in private Working Memory
|
|
10
|
+
{
|
|
11
|
+
"run": "degree-1",
|
|
12
|
+
"knowledgeAsset": "xrun-cfd92f38836000da91c750b60b21b2d6d719976279beaae0",
|
|
13
|
+
"mse": 0.7939276145811444,
|
|
14
|
+
"layer": "working-memory",
|
|
15
|
+
"duplicate": false
|
|
16
|
+
}
|
|
17
|
+
{
|
|
18
|
+
"run": "degree-3",
|
|
19
|
+
"knowledgeAsset": "xrun-a723e3ccfa7adf9a2efd0d519f8c5f399e06d80b7abad449",
|
|
20
|
+
"mse": 0.004958052031565096,
|
|
21
|
+
"layer": "working-memory",
|
|
22
|
+
"duplicate": false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
2. Replay the identical record: same asset, no second payload
|
|
26
|
+
{
|
|
27
|
+
"duplicate": true,
|
|
28
|
+
"sameName": true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
3. Retrieve the stored runs from DKG and compare compatible metrics
|
|
32
|
+
{
|
|
33
|
+
"baseline": "degree-1",
|
|
34
|
+
"candidate": "degree-3",
|
|
35
|
+
"metrics": [
|
|
36
|
+
{
|
|
37
|
+
"name": "mse",
|
|
38
|
+
"unit": "squared-error",
|
|
39
|
+
"direction": "minimize",
|
|
40
|
+
"baseline": 0.7939276145811444,
|
|
41
|
+
"candidate": 0.004958052031565096,
|
|
42
|
+
"delta": -0.7889695625495793,
|
|
43
|
+
"relativeChange": -0.9937550326496947,
|
|
44
|
+
"improved": true
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"parameterChanges": [
|
|
48
|
+
{
|
|
49
|
+
"name": "degree",
|
|
50
|
+
"baseline": 1,
|
|
51
|
+
"candidate": 3
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"sourceChanged": false,
|
|
55
|
+
"interpretation": "Observed differences only; no causal or statistical significance claim."
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
4. Explicitly SHARE the candidate under the Project Curator authority
|
|
59
|
+
{
|
|
60
|
+
"swmShared": true,
|
|
61
|
+
"sealed": true,
|
|
62
|
+
"publishReady": true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
5. Recall the candidate from Shared Memory using project-scoped SPARQL
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
"run": "degree-3",
|
|
69
|
+
"digest": "a723e3ccfa7adf9a2efd0d519f8c5f399e06d80b7abad44933f529ffdb90f901",
|
|
70
|
+
"layer": "shared-working-memory",
|
|
71
|
+
"trust": "self-attested"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
6. Promotion path: official agent conversation -> PUBLISH -> actual UAL
|
|
76
|
+
No on-chain publication, spending, or independent verification claimed.
|
|
77
|
+
All outputs above came from the running local DKG 10.0.16 API.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{"version": 2, "width": 100, "height": 36, "title": "DKG Experiment Ledger \u2014 actual local persistent-node demo", "env": {"TERM": "xterm-256color"}}
|
|
2
|
+
[0.083, "o", "DKG EXPERIMENT LEDGER | live local API walkthrough\r\n"]
|
|
3
|
+
[0.083, "o", "{\r\n"]
|
|
4
|
+
[0.083, "o", " \"nodeVersion\": \"10.0.16\",\r\n"]
|
|
5
|
+
[0.083, "o", " \"project\": \"astra-recorded-demo-final\",\r\n"]
|
|
6
|
+
[0.083, "o", " \"connectedPeers\": 0,\r\n"]
|
|
7
|
+
[0.083, "o", " \"chain\": \"Isolated lab mock chain; no public publication\"\r\n"]
|
|
8
|
+
[0.083, "o", "}\r\n"]
|
|
9
|
+
[2.085, "o", "\r\n"]
|
|
10
|
+
[2.085, "o", "1. Record two measured regression runs in private Working Memory\r\n"]
|
|
11
|
+
[2.114, "o", "{\r\n"]
|
|
12
|
+
[2.114, "o", " \"run\": \"degree-1\",\r\n"]
|
|
13
|
+
[2.114, "o", " \"knowledgeAsset\": \"xrun-cfd92f38836000da91c750b60b21b2d6d719976279beaae0\",\r\n"]
|
|
14
|
+
[2.114, "o", " \"mse\": 0.7939276145811444,\r\n"]
|
|
15
|
+
[2.114, "o", " \"layer\": \"working-memory\",\r\n"]
|
|
16
|
+
[2.114, "o", " \"duplicate\": false\r\n"]
|
|
17
|
+
[2.114, "o", "}\r\n"]
|
|
18
|
+
[2.123, "o", "{\r\n"]
|
|
19
|
+
[2.123, "o", " \"run\": \"degree-3\",\r\n"]
|
|
20
|
+
[2.123, "o", " \"knowledgeAsset\": \"xrun-a723e3ccfa7adf9a2efd0d519f8c5f399e06d80b7abad449\",\r\n"]
|
|
21
|
+
[2.123, "o", " \"mse\": 0.004958052031565096,\r\n"]
|
|
22
|
+
[2.123, "o", " \"layer\": \"working-memory\",\r\n"]
|
|
23
|
+
[2.123, "o", " \"duplicate\": false\r\n"]
|
|
24
|
+
[2.123, "o", "}\r\n"]
|
|
25
|
+
[4.125, "o", "\r\n"]
|
|
26
|
+
[4.125, "o", "2. Replay the identical record: same asset, no second payload\r\n"]
|
|
27
|
+
[4.127, "o", "{\r\n"]
|
|
28
|
+
[4.127, "o", " \"duplicate\": true,\r\n"]
|
|
29
|
+
[4.127, "o", " \"sameName\": true\r\n"]
|
|
30
|
+
[4.127, "o", "}\r\n"]
|
|
31
|
+
[6.13, "o", "\r\n"]
|
|
32
|
+
[6.13, "o", "3. Retrieve the stored runs from DKG and compare compatible metrics\r\n"]
|
|
33
|
+
[6.138, "o", "{\r\n"]
|
|
34
|
+
[6.138, "o", " \"baseline\": \"degree-1\",\r\n"]
|
|
35
|
+
[6.138, "o", " \"candidate\": \"degree-3\",\r\n"]
|
|
36
|
+
[6.138, "o", " \"metrics\": [\r\n"]
|
|
37
|
+
[6.138, "o", " {\r\n"]
|
|
38
|
+
[6.138, "o", " \"name\": \"mse\",\r\n"]
|
|
39
|
+
[6.138, "o", " \"unit\": \"squared-error\",\r\n"]
|
|
40
|
+
[6.138, "o", " \"direction\": \"minimize\",\r\n"]
|
|
41
|
+
[6.138, "o", " \"baseline\": 0.7939276145811444,\r\n"]
|
|
42
|
+
[6.138, "o", " \"candidate\": 0.004958052031565096,\r\n"]
|
|
43
|
+
[6.139, "o", " \"delta\": -0.7889695625495793,\r\n"]
|
|
44
|
+
[6.139, "o", " \"relativeChange\": -0.9937550326496947,\r\n"]
|
|
45
|
+
[6.139, "o", " \"improved\": true\r\n"]
|
|
46
|
+
[6.139, "o", " }\r\n"]
|
|
47
|
+
[6.139, "o", " ],\r\n"]
|
|
48
|
+
[6.139, "o", " \"parameterChanges\": [\r\n"]
|
|
49
|
+
[6.139, "o", " {\r\n"]
|
|
50
|
+
[6.139, "o", " \"name\": \"degree\",\r\n"]
|
|
51
|
+
[6.139, "o", " \"baseline\": 1,\r\n"]
|
|
52
|
+
[6.139, "o", " \"candidate\": 3\r\n"]
|
|
53
|
+
[6.139, "o", " }\r\n"]
|
|
54
|
+
[6.139, "o", " ],\r\n"]
|
|
55
|
+
[6.139, "o", " \"sourceChanged\": false,\r\n"]
|
|
56
|
+
[6.139, "o", " \"interpretation\": \"Observed differences only; no causal or statistical significance claim.\"\r\n"]
|
|
57
|
+
[6.139, "o", "}\r\n"]
|
|
58
|
+
[8.14, "o", "\r\n"]
|
|
59
|
+
[8.14, "o", "4. Explicitly SHARE the candidate under the Project Curator authority\r\n"]
|
|
60
|
+
[8.167, "o", "{\r\n"]
|
|
61
|
+
[8.167, "o", " \"swmShared\": true,\r\n"]
|
|
62
|
+
[8.167, "o", " \"sealed\": true,\r\n"]
|
|
63
|
+
[8.167, "o", " \"publishReady\": true\r\n"]
|
|
64
|
+
[8.167, "o", "}\r\n"]
|
|
65
|
+
[10.17, "o", "\r\n"]
|
|
66
|
+
[10.17, "o", "5. Recall the candidate from Shared Memory using project-scoped SPARQL\r\n"]
|
|
67
|
+
[10.186, "o", "[\r\n"]
|
|
68
|
+
[10.186, "o", " {\r\n"]
|
|
69
|
+
[10.186, "o", " \"run\": \"degree-3\",\r\n"]
|
|
70
|
+
[10.186, "o", " \"digest\": \"a723e3ccfa7adf9a2efd0d519f8c5f399e06d80b7abad44933f529ffdb90f901\",\r\n"]
|
|
71
|
+
[10.186, "o", " \"layer\": \"shared-working-memory\",\r\n"]
|
|
72
|
+
[10.186, "o", " \"trust\": \"self-attested\"\r\n"]
|
|
73
|
+
[10.186, "o", " }\r\n"]
|
|
74
|
+
[10.186, "o", "]\r\n"]
|
|
75
|
+
[12.188, "o", "\r\n"]
|
|
76
|
+
[12.188, "o", "6. Promotion path: official agent conversation -> PUBLISH -> actual UAL\r\n"]
|
|
77
|
+
[12.188, "o", "No on-chain publication, spending, or independent verification claimed.\r\n"]
|
|
78
|
+
[12.188, "o", "All outputs above came from the running local DKG 10.0.16 API.\r\n"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {readFile,writeFile,mkdir} from 'node:fs/promises';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {ExperimentLedger,compareRecords} from '../src/ledger.mjs';
|
|
4
|
+
import {sha256,canonical} from '../src/record.mjs';
|
|
5
|
+
import {loadToken} from '../src/token.mjs';
|
|
6
|
+
|
|
7
|
+
// A deterministic, real regression experiment. Train on alternating points,
|
|
8
|
+
// evaluate on the held-out points. No downloaded data, LLM, or paid API.
|
|
9
|
+
const dataset=Array.from({length:80},(_,i)=>{const x=-2+4*i/79;return [x,0.7*x*x*x-0.5*x+0.1*Math.sin(7*x)];});
|
|
10
|
+
const train=dataset.filter((_,i)=>i%2===0),heldout=dataset.filter((_,i)=>i%2===1);
|
|
11
|
+
function fit(degree){
|
|
12
|
+
const n=degree+1,a=Array.from({length:n},()=>Array(n+1).fill(0));
|
|
13
|
+
for(const [x,y] of train)for(let i=0;i<n;i++){for(let j=0;j<n;j++)a[i][j]+=x**(i+j);a[i][n]+=y*x**i;}
|
|
14
|
+
for(let p=0;p<n;p++){
|
|
15
|
+
let pivot=p;for(let i=p+1;i<n;i++)if(Math.abs(a[i][p])>Math.abs(a[pivot][p]))pivot=i;
|
|
16
|
+
[a[p],a[pivot]]=[a[pivot],a[p]];const scale=a[p][p];if(Math.abs(scale)<1e-12)throw new Error('Singular fit');
|
|
17
|
+
for(let j=p;j<=n;j++)a[p][j]/=scale;
|
|
18
|
+
for(let i=0;i<n;i++)if(i!==p){const factor=a[i][p];for(let j=p;j<=n;j++)a[i][j]-=factor*a[p][j];}
|
|
19
|
+
}
|
|
20
|
+
return a.map(row=>row[n]);
|
|
21
|
+
}
|
|
22
|
+
const codeHash=sha256(await readFile(fileURLToPath(import.meta.url))),datasetHash=sha256(canonical(dataset));
|
|
23
|
+
const records=[1,3].map(degree=>{
|
|
24
|
+
const coefficients=fit(degree),mse=heldout.reduce((sum,[x,y])=>sum+(coefficients.reduce((v,c,i)=>v+c*x**i,0)-y)**2,0)/heldout.length;
|
|
25
|
+
return {schemaVersion:1,experiment:'polynomial-generalization',runId:`degree-${degree}`,dataset:{name:'synthetic-cubic-80-alternating-holdout-v1',fingerprint:datasetHash},code:{revision:codeHash},parameters:{degree,trainSize:train.length,holdoutSize:heldout.length},metrics:{mse:{value:mse,unit:'squared-error',direction:'minimize'}},observedAt:new Date().toISOString(),author:'autoresearch-demo-agent',command:['node','examples/autoresearch.mjs'],notes:'Measured on deterministic held-out synthetic data. Not a production model benchmark.'};
|
|
26
|
+
});
|
|
27
|
+
await mkdir('demo-output',{recursive:true});
|
|
28
|
+
for(const r of records)await writeFile(`demo-output/${r.runId}.json`,JSON.stringify(r,null,2));
|
|
29
|
+
if(process.argv.includes('--offline')){console.log(JSON.stringify({records,comparison:compareRecords(...records)},null,2));}
|
|
30
|
+
else {
|
|
31
|
+
const token=await loadToken();
|
|
32
|
+
const ledger=new ExperimentLedger({baseUrl:process.env.DKG_URL,token,contextGraphId:process.env.DKG_CONTEXT_GRAPH});
|
|
33
|
+
const saved=[];for(const r of records)saved.push(await ledger.record(r));
|
|
34
|
+
const replay=saved.map(s=>({name:s.name,digest:s.digest,layer:s.layer}));
|
|
35
|
+
const comparison=compareRecords((await ledger.get(saved[0].name)).record,(await ledger.get(saved[1].name)).record);
|
|
36
|
+
const result={saved:replay,comparison,retrievedProjectRuns:(await ledger.list()).length,promotionPlan:ledger.promotionPlan(saved[1].name)};
|
|
37
|
+
await writeFile('demo-output/receipt.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result,null,2));
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Standard-library bridge for a Jupyter/autoresearch Python kernel.
|
|
2
|
+
|
|
3
|
+
Only the explicit record argument is sent. No notebook history, environment,
|
|
4
|
+
filesystem crawling, git identity, or automatic execution is collected.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def record_experiment(record, *, cli=None):
|
|
13
|
+
"""Write one record through the installed CLI, returning the DKG receipt."""
|
|
14
|
+
import tempfile
|
|
15
|
+
cli = cli or ["dkg-experiments"]
|
|
16
|
+
# Only required runtime variables are forwarded, never the kernel's full env.
|
|
17
|
+
environment = {k: v for k, v in os.environ.items() if k in {
|
|
18
|
+
"PATH", "SYSTEMROOT", "TMPDIR", "LANG", "DKG_URL", "DKG_TOKEN", "DKG_API_URL", "DKG_AUTH_TOKEN",
|
|
19
|
+
"DKG_TOKEN_FILE", "DKG_CONTEXT_GRAPH", "DKG_AGENT_ADDRESS", "DKG_ALLOW_REMOTE"
|
|
20
|
+
}}
|
|
21
|
+
with tempfile.TemporaryDirectory(prefix="dkg-run-") as tmp:
|
|
22
|
+
record_file = Path(tmp) / "record.json"
|
|
23
|
+
record_file.write_text(json.dumps(record, allow_nan=False), encoding="utf-8")
|
|
24
|
+
record_file.chmod(0o600)
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
[*cli, "record", str(record_file)], env=environment,
|
|
27
|
+
capture_output=True, text=True, check=False, timeout=60,
|
|
28
|
+
)
|
|
29
|
+
if result.returncode:
|
|
30
|
+
raise RuntimeError(result.stderr.strip() or "DKG record failed")
|
|
31
|
+
return json.loads(result.stdout)
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@btusbefw/dkg-experiment-ledger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reproducible experiment records in OriginTrail DKG v10 Working Memory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dkg-experiments": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": "./src/ledger.mjs",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.mjs",
|
|
15
|
+
"test:integration": "node --test integration/*.test.mjs",
|
|
16
|
+
"demo": "node examples/autoresearch.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"src",
|
|
21
|
+
"demo",
|
|
22
|
+
"README.md",
|
|
23
|
+
"DESIGN.md",
|
|
24
|
+
"SECURITY.md",
|
|
25
|
+
"LAB.md",
|
|
26
|
+
"DEMO.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"examples/*.mjs",
|
|
29
|
+
"examples/*.py",
|
|
30
|
+
"scripts/demo-session.mjs"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/btusbefw/dkg-experiment-ledger.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/btusbefw/dkg-experiment-ledger#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/btusbefw/dkg-experiment-ledger/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {ExperimentLedger,compareRecords} from '../src/ledger.mjs';
|
|
3
|
+
import {loadToken} from '../src/token.mjs';
|
|
4
|
+
const pause=()=>new Promise(r=>setTimeout(r,2000));
|
|
5
|
+
const print=value=>console.log(JSON.stringify(value,null,2));
|
|
6
|
+
const client=new ExperimentLedger({baseUrl:process.env.DKG_URL,token:await loadToken(),contextGraphId:process.env.DKG_CONTEXT_GRAPH});
|
|
7
|
+
const status=await client.request('/api/status');
|
|
8
|
+
console.log('DKG EXPERIMENT LEDGER | live local API walkthrough');
|
|
9
|
+
print({nodeVersion:status.version,project:client.contextGraphId,connectedPeers:status.connectedPeers,chain:'Isolated lab mock chain; no public publication'});
|
|
10
|
+
await pause();
|
|
11
|
+
console.log('\n1. Record two measured regression runs in private Working Memory');
|
|
12
|
+
const saved=[];
|
|
13
|
+
for(const name of ['degree-1','degree-3']){
|
|
14
|
+
const record=JSON.parse(await readFile(`demo-output/${name}.json`,'utf8'));
|
|
15
|
+
const receipt=await client.record(record);saved.push(receipt);
|
|
16
|
+
print({run:record.runId,knowledgeAsset:receipt.name,mse:receipt.record.metrics.mse.value,layer:receipt.layer,duplicate:receipt.duplicate});
|
|
17
|
+
}
|
|
18
|
+
await pause();
|
|
19
|
+
console.log('\n2. Replay the identical record: same asset, no second payload');
|
|
20
|
+
const duplicate=await client.record(saved[0].record);print({duplicate:duplicate.duplicate,sameName:duplicate.name===saved[0].name});
|
|
21
|
+
await pause();
|
|
22
|
+
console.log('\n3. Retrieve the stored runs from DKG and compare compatible metrics');
|
|
23
|
+
const comparison=compareRecords((await client.get(saved[0].name)).record,(await client.get(saved[1].name)).record);
|
|
24
|
+
print(comparison);
|
|
25
|
+
await pause();
|
|
26
|
+
console.log('\n4. Explicitly SHARE the candidate under the Project Curator authority');
|
|
27
|
+
const shared=await client.share(saved[1].name,{confirmContextGraphId:client.contextGraphId});
|
|
28
|
+
print({swmShared:shared.swmShared,sealed:shared.sealed,publishReady:shared.publishReady});
|
|
29
|
+
await pause();
|
|
30
|
+
console.log('\n5. Recall the candidate from Shared Memory using project-scoped SPARQL');
|
|
31
|
+
const rows=await client.list({view:'shared-working-memory'});
|
|
32
|
+
print(rows.map(r=>({run:r.record.runId,digest:r.digest,layer:r.layer,trust:r.trust})));
|
|
33
|
+
await pause();
|
|
34
|
+
console.log('\n6. Promotion path: official agent conversation -> PUBLISH -> actual UAL');
|
|
35
|
+
console.log('No on-chain publication, spending, or independent verification claimed.');
|
|
36
|
+
console.log('All outputs above came from the running local DKG 10.0.16 API.');
|
|
37
|
+
await pause();
|
package/src/ledger.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {encodeRecord, validateRecord, canonical, NS, compareRecords} from './record.mjs';
|
|
2
|
+
export {encodeRecord,validateRecord,compareRecords};
|
|
3
|
+
|
|
4
|
+
export class DkgError extends Error {
|
|
5
|
+
constructor(status, operation) {super(`DKG ${operation} failed (HTTP ${status}); inspect the node privately`);this.status=status;}
|
|
6
|
+
}
|
|
7
|
+
class EmptyRecordError extends Error {}
|
|
8
|
+
export class ExperimentLedger {
|
|
9
|
+
constructor({baseUrl='http://127.0.0.1:9200',token,contextGraphId,agentAddress,allowRemote=false,fetchImpl=fetch}) {
|
|
10
|
+
const u=new URL(baseUrl);
|
|
11
|
+
if (u.username || u.password || u.search || u.hash || !['http:','https:'].includes(u.protocol) || u.pathname!=='/') throw new Error('Invalid DKG base URL');
|
|
12
|
+
const loopback=['127.0.0.1','[::1]','localhost'].includes(u.hostname);
|
|
13
|
+
if (!loopback && (!allowRemote || u.protocol!=='https:')) throw new Error('Remote DKG requires explicit allowRemote and HTTPS');
|
|
14
|
+
if (typeof token!=='string' || !token.trim() || /[\r\n]/.test(token)) throw new Error('DKG token required');
|
|
15
|
+
if (typeof contextGraphId!=='string' || !contextGraphId.trim() || contextGraphId.length>500) throw new Error('Exact contextGraphId required');
|
|
16
|
+
this.baseUrl=u.origin;this.token=token;this.contextGraphId=contextGraphId;this.agentAddress=agentAddress;this.fetch=fetchImpl;
|
|
17
|
+
}
|
|
18
|
+
async request(path,body) {
|
|
19
|
+
const res=await this.fetch(this.baseUrl+path,{method:body===undefined?'GET':'POST',headers:{Authorization:`Bearer ${this.token}`,...(body===undefined?{}:{'Content-Type':'application/json'})},body:body===undefined?undefined:JSON.stringify(body),redirect:'error',signal:AbortSignal.timeout(15000)});
|
|
20
|
+
if (!res.ok) throw new DkgError(res.status,path.split('?')[0]);
|
|
21
|
+
const data=await res.json();
|
|
22
|
+
if (data.error) throw new Error('DKG reported an operation error; inspect the node privately');
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
path(name,suffix='') {
|
|
26
|
+
if (!/^xrun-[a-f0-9]{48}$/.test(name)) throw new Error('Invalid ledger Knowledge Asset name');
|
|
27
|
+
return `/api/knowledge-assets/${name}${suffix}`;
|
|
28
|
+
}
|
|
29
|
+
params(){return new URLSearchParams({contextGraphId:this.contextGraphId,...(this.agentAddress?{agentAddress:this.agentAddress}:{})});}
|
|
30
|
+
async record(input) {
|
|
31
|
+
const e=encodeRecord(input),body={contextGraphId:this.contextGraphId,name:e.name};
|
|
32
|
+
// DKG 10.0.16's alreadyExists flag can miss custodial-agent records.
|
|
33
|
+
// Reconcile the deterministic content before any create/write mutation.
|
|
34
|
+
try {
|
|
35
|
+
const prior=await this.get(e.name);
|
|
36
|
+
if(canonical(prior.record)!==e.payload)throw new Error('Existing Knowledge Asset does not match this record');
|
|
37
|
+
return {...prior,duplicate:true};
|
|
38
|
+
} catch(err) {
|
|
39
|
+
if(!(err instanceof EmptyRecordError) && err.status!==404)throw err;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await this.request(this.path(e.name)+'?'+this.params());
|
|
43
|
+
throw new Error('Knowledge Asset exists without a readable WM payload; inspect its lifecycle before recovery');
|
|
44
|
+
} catch(err) {if(err.status!==404)throw err;}
|
|
45
|
+
let existing=false;
|
|
46
|
+
try {existing=(await this.request('/api/knowledge-assets',body)).alreadyExists===true;} catch(err) {if(err.status!==409) throw err;existing=true;}
|
|
47
|
+
if (existing) {
|
|
48
|
+
const prior=await this.get(e.name);
|
|
49
|
+
if (canonical(prior.record)!==e.payload) throw new Error('Existing Knowledge Asset does not match this record');
|
|
50
|
+
return {...prior,duplicate:true};
|
|
51
|
+
}
|
|
52
|
+
// No blind retry of mutations. If this request times out, inspect the same
|
|
53
|
+
// deterministic KA name; never create another asset under a random name.
|
|
54
|
+
await this.request(this.path(e.name,'/wm/write'),{contextGraphId:this.contextGraphId,quads:e.quads});
|
|
55
|
+
const saved=await this.get(e.name);
|
|
56
|
+
if (canonical(saved.record)!==e.payload) throw new Error('Read-after-write verification failed');
|
|
57
|
+
return {...saved,duplicate:false};
|
|
58
|
+
}
|
|
59
|
+
async get(name) {
|
|
60
|
+
const data=await this.request(this.path(name,'/wm/quads')+'?'+this.params());
|
|
61
|
+
const quads=Array.isArray(data)?data:data.quads;
|
|
62
|
+
if (!Array.isArray(quads)) throw new Error('Unsupported DKG quad response');
|
|
63
|
+
const q=quads.find(q=>q.predicate===NS+'payload');
|
|
64
|
+
if (!q) {
|
|
65
|
+
if(quads.length===0)throw new EmptyRecordError('No Working Memory record at this name; inspect its lifecycle if previously shared');
|
|
66
|
+
throw new Error('Incomplete record: Knowledge Asset exists without a ledger payload; inspect before recovery');
|
|
67
|
+
}
|
|
68
|
+
// Node returns RDF lexical representation, not a pre-parsed JSON object.
|
|
69
|
+
let payload;
|
|
70
|
+
try {payload=JSON.parse(q.object);} catch {throw new Error('Malformed payload literal');}
|
|
71
|
+
const e=encodeRecord(JSON.parse(payload));
|
|
72
|
+
if(e.name!==name || e.subject!==q.subject) throw new Error('Record content digest mismatch');
|
|
73
|
+
return {name,digest:e.digest,record:e.record,contextGraphId:this.contextGraphId,layer:'working-memory',trust:'self-attested'};
|
|
74
|
+
}
|
|
75
|
+
async list({limit=100,view='working-memory'}={}) {
|
|
76
|
+
if (!Number.isInteger(limit)||limit<1||limit>1000) throw new Error('limit must be 1–1000');
|
|
77
|
+
if(!['working-memory','shared-working-memory'].includes(view)) throw new Error('Unsupported memory view');
|
|
78
|
+
const data=await this.request('/api/query',{sparql:`SELECT ?subject ?payload WHERE { ?subject <${NS}payload> ?payload . } ORDER BY ?subject LIMIT ${limit}`,contextGraphId:this.contextGraphId,view,...(this.agentAddress?{agentAddress:this.agentAddress}:{})});
|
|
79
|
+
const bindings=data.result?.bindings??data.results?.bindings??data.bindings??data.results;
|
|
80
|
+
if(!Array.isArray(bindings)) throw new Error('Unsupported DKG query response');
|
|
81
|
+
return bindings.map(row=>{
|
|
82
|
+
const value=v=>typeof v==='object'?v.value:v;
|
|
83
|
+
const payload=value(row.payload),subject=value(row.subject);
|
|
84
|
+
const parsed=JSON.parse(payload),e=encodeRecord(typeof parsed==='string'?JSON.parse(parsed):parsed);
|
|
85
|
+
if(subject!==e.subject) throw new Error('Query result digest mismatch');
|
|
86
|
+
return {name:e.name,digest:e.digest,record:e.record,contextGraphId:this.contextGraphId,layer:view,trust:'self-attested'};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async share(name,{confirmContextGraphId}={}) {
|
|
90
|
+
if(confirmContextGraphId!==this.contextGraphId) throw new Error('SHARE requires confirmation of the exact Context Graph ID');
|
|
91
|
+
await this.get(name);
|
|
92
|
+
// Curator permissions are enforced by DKG. No fallback to public graphs,
|
|
93
|
+
// skipSeal, alternate identities, or direct Shared Memory writes.
|
|
94
|
+
return this.request(this.path(name,'/swm/share'),{contextGraphId:this.contextGraphId,entities:'all'});
|
|
95
|
+
}
|
|
96
|
+
promotionPlan(name) {
|
|
97
|
+
this.path(name);
|
|
98
|
+
return {name,contextGraphId:this.contextGraphId,steps:['Review source provenance and reproducibility; record remains self-attested.','Ask the Context Graph Curator to authorize SHARE; the node seals the full asset.','When ready, use the official DKG agent conversation to authorize PUBLISH and its fees.','Store the actual UAL returned by DKG with the downstream oracle citation; never invent a UAL.'],publicationPerformed:false};
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/record.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const NS = 'https://w3id.org/dkg-experiment-ledger/v1/';
|
|
4
|
+
const MAX_BYTES = 128 * 1024;
|
|
5
|
+
const safeKey = /^[A-Za-z][A-Za-z0-9_.-]{0,79}$/;
|
|
6
|
+
const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const fail = message => { throw new Error(message); };
|
|
8
|
+
function text(value, name, max = 500) {
|
|
9
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value)) fail(`Invalid ${name}`);
|
|
10
|
+
}
|
|
11
|
+
function keys(value, allowed, name) {
|
|
12
|
+
if (!plain(value) || Object.keys(value).some(k => !allowed.includes(k))) fail(`Invalid or unknown fields in ${name}`);
|
|
13
|
+
}
|
|
14
|
+
export function canonical(value) {
|
|
15
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
16
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
17
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
18
|
+
}
|
|
19
|
+
export const sha256 = value => createHash('sha256').update(value).digest('hex');
|
|
20
|
+
|
|
21
|
+
export function validateRecord(input) {
|
|
22
|
+
// JSON round-tripping also rejects cycles/BigInt and prevents custom prototypes.
|
|
23
|
+
const encoded = JSON.stringify(input);
|
|
24
|
+
if (!encoded || Buffer.byteLength(encoded) > MAX_BYTES) fail('Record must be JSON and at most 128 KiB');
|
|
25
|
+
// Reject non-finite numbers before JSON.stringify can silently convert them to null.
|
|
26
|
+
function finite(v, depth = 0) {
|
|
27
|
+
if (depth > 12) fail('Record is nested too deeply');
|
|
28
|
+
if (typeof v === 'number' && !Number.isFinite(v)) fail('Numbers must be finite');
|
|
29
|
+
if (typeof v === 'undefined' || typeof v === 'function' || typeof v === 'symbol') fail('Values must be JSON');
|
|
30
|
+
if (v && typeof v === 'object') for (const [k, child] of Object.entries(v)) {
|
|
31
|
+
if (['__proto__', 'constructor', 'prototype'].includes(k)) fail('Reserved object key');
|
|
32
|
+
finite(child, depth + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
finite(input);
|
|
36
|
+
const r = JSON.parse(encoded);
|
|
37
|
+
keys(r, ['schemaVersion','experiment','runId','dataset','code','parameters','metrics','observedAt','author','command','notes','artifacts'], 'record');
|
|
38
|
+
if (r.schemaVersion !== 1) fail('schemaVersion must be 1');
|
|
39
|
+
text(r.experiment, 'experiment', 160); text(r.runId, 'runId', 160); text(r.author, 'author', 160);
|
|
40
|
+
if (typeof r.observedAt !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(r.observedAt) || !Number.isFinite(Date.parse(r.observedAt))) fail('observedAt must be a UTC ISO timestamp');
|
|
41
|
+
if(new Date(r.observedAt).toISOString().replace('.000Z','Z')!==r.observedAt.replace('.000Z','Z'))fail('observedAt must be an actual calendar timestamp');
|
|
42
|
+
keys(r.dataset, ['name','fingerprint'], 'dataset'); text(r.dataset.name,'dataset.name');
|
|
43
|
+
if (!/^[a-f0-9]{64}$/.test(r.dataset.fingerprint ?? '')) fail('dataset.fingerprint must be SHA-256');
|
|
44
|
+
keys(r.code, ['revision','repository'], 'code');
|
|
45
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(r.code.revision ?? '')) fail('code.revision must be a full Git revision or source SHA-256');
|
|
46
|
+
function url(v) {
|
|
47
|
+
const u = new URL(v);
|
|
48
|
+
if (u.protocol !== 'https:' || u.username || u.password || u.search || u.hash) fail('Artifact/repository URLs must be HTTPS without credentials, query, or fragment');
|
|
49
|
+
}
|
|
50
|
+
if (r.code.repository !== undefined) url(r.code.repository);
|
|
51
|
+
if (!plain(r.parameters) || Object.keys(r.parameters).length > 100) fail('parameters must be an object with at most 100 keys');
|
|
52
|
+
if (!plain(r.metrics) || !Object.keys(r.metrics).length || Object.keys(r.metrics).length > 100) fail('metrics must have 1–100 entries');
|
|
53
|
+
for (const [k, m] of Object.entries(r.metrics)) {
|
|
54
|
+
if (!safeKey.test(k)) fail('Invalid metric name');
|
|
55
|
+
keys(m, ['value','unit','direction'], `metric ${k}`);
|
|
56
|
+
if (typeof m.value !== 'number' || !Number.isFinite(m.value)) fail('Metric values must be finite numbers');
|
|
57
|
+
text(m.unit, 'metric unit', 80);
|
|
58
|
+
if (!['minimize','maximize'].includes(m.direction)) fail('Metric direction must be minimize or maximize');
|
|
59
|
+
}
|
|
60
|
+
for (const k of Object.keys(r.parameters)) if (!safeKey.test(k)) fail('Invalid parameter name');
|
|
61
|
+
if (r.notes !== undefined) text(r.notes,'notes',12000);
|
|
62
|
+
if (r.command !== undefined && (!Array.isArray(r.command) || !r.command.length || r.command.length > 100 || r.command.some(v => typeof v !== 'string' || v.length > 2000))) fail('command must be an array of strings');
|
|
63
|
+
if (r.artifacts !== undefined) {
|
|
64
|
+
if (!Array.isArray(r.artifacts) || r.artifacts.length > 100) fail('Invalid artifacts');
|
|
65
|
+
for (const a of r.artifacts) {
|
|
66
|
+
keys(a, ['name','sha256','mediaType','uri'], 'artifact'); text(a.name,'artifact.name');
|
|
67
|
+
if (!/^[a-f0-9]{64}$/.test(a.sha256 ?? '')) fail('Artifact hash must be SHA-256');
|
|
68
|
+
if (a.mediaType !== undefined) text(a.mediaType,'artifact.mediaType',100);
|
|
69
|
+
if (a.uri !== undefined) url(a.uri);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return r;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function encodeRecord(input) {
|
|
76
|
+
const record = validateRecord(input), payload = canonical(record), digest = sha256(payload);
|
|
77
|
+
const subject = `urn:sha256:${digest}`, name = `xrun-${digest.slice(0,48)}`;
|
|
78
|
+
const literal = v => JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
79
|
+
const quad = (predicate, object) => ({subject, predicate, object});
|
|
80
|
+
const quads = [
|
|
81
|
+
quad('http://www.w3.org/1999/02/22-rdf-syntax-ns#type', NS+'ExperimentRun'),
|
|
82
|
+
quad(NS+'payload',literal(payload)), quad(NS+'digest',literal(digest)),
|
|
83
|
+
quad(NS+'experiment',literal(record.experiment)), quad(NS+'runId',literal(record.runId)),
|
|
84
|
+
quad(NS+'datasetFingerprint',literal(record.dataset.fingerprint)),
|
|
85
|
+
quad('http://www.w3.org/ns/prov#wasAttributedTo',literal(record.author)),
|
|
86
|
+
quad('http://www.w3.org/ns/prov#generatedAtTime',literal(record.observedAt)),
|
|
87
|
+
quad(NS+'sourceRevision',literal(record.code.revision))
|
|
88
|
+
];
|
|
89
|
+
for (const [metric, m] of Object.entries(record.metrics)) {
|
|
90
|
+
quads.push(quad(NS+'metric/'+metric, `"${m.value}"^^<http://www.w3.org/2001/XMLSchema#double>`));
|
|
91
|
+
}
|
|
92
|
+
return {record,payload,digest,subject,name,quads};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function compareRecords(baseInput, candidateInput) {
|
|
96
|
+
const baseline = validateRecord(baseInput), candidate = validateRecord(candidateInput);
|
|
97
|
+
if (baseline.experiment !== candidate.experiment || baseline.dataset.fingerprint !== candidate.dataset.fingerprint) fail('Runs require the same experiment and dataset fingerprint');
|
|
98
|
+
const shared = Object.keys(baseline.metrics).filter(k => Object.hasOwn(candidate.metrics,k));
|
|
99
|
+
if (!shared.length) fail('Runs have no common metrics');
|
|
100
|
+
const metrics = shared.map(name => {
|
|
101
|
+
const a=baseline.metrics[name],b=candidate.metrics[name];
|
|
102
|
+
if (a.unit !== b.unit || a.direction !== b.direction) fail(`Incompatible metric ${name}: unit or direction differs`);
|
|
103
|
+
const delta=b.value-a.value;
|
|
104
|
+
return {name,unit:a.unit,direction:a.direction,baseline:a.value,candidate:b.value,delta,relativeChange:a.value===0?null:delta/Math.abs(a.value),improved:delta===0?null:(a.direction==='minimize'?delta<0:delta>0)};
|
|
105
|
+
});
|
|
106
|
+
const parameterChanges = [...new Set([...Object.keys(baseline.parameters),...Object.keys(candidate.parameters)])].sort().filter(k=>canonical(baseline.parameters[k])!==canonical(candidate.parameters[k])).map(name=>({name,baseline:baseline.parameters[name]??null,candidate:candidate.parameters[name]??null}));
|
|
107
|
+
return {baseline:baseline.runId,candidate:candidate.runId,metrics,parameterChanges,sourceChanged:baseline.code.revision!==candidate.code.revision,interpretation:'Observed differences only; no causal or statistical significance claim.'};
|
|
108
|
+
}
|
package/src/token.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
export function parseTokenFile(contents) {
|
|
3
|
+
const lines=contents.split(/\r?\n/).map(s=>s.trim()).filter(s=>s&&!s.startsWith('#'));
|
|
4
|
+
if(lines.length!==1 || !/^[A-Za-z0-9_.~-]+$/.test(lines[0])) throw new Error('Token file must contain exactly one bearer token, with optional comment lines');
|
|
5
|
+
return lines[0];
|
|
6
|
+
}
|
|
7
|
+
export async function loadToken(env=process.env){
|
|
8
|
+
if(env.DKG_TOKEN||env.DKG_AUTH_TOKEN)return env.DKG_TOKEN||env.DKG_AUTH_TOKEN;
|
|
9
|
+
if(!env.DKG_TOKEN_FILE)throw new Error('Set DKG_TOKEN_FILE to the local node token file or set DKG_TOKEN');
|
|
10
|
+
return parseTokenFile(await readFile(env.DKG_TOKEN_FILE,'utf8'));
|
|
11
|
+
}
|