diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f219e85 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "CommitLedger Canton Proof", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "22" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "21" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + } + }, + "postCreateCommand": "bash scripts/bootstrap-codespace.sh", + "forwardPorts": [4173, 3975], + "portsAttributes": { + "4173": { + "label": "CommitLedger judge UI", + "onAutoForward": "notify" + }, + "3975": { + "label": "Canton JSON Ledger API", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "extensions": [] + } + } +} diff --git a/.env.example b/.env.example index 1d9e4bc..03eacf3 100644 --- a/.env.example +++ b/.env.example @@ -1,27 +1,18 @@ -# Canton JSON Ledger API. -CANTON_JSON_API_URL=http://localhost:3975 +# Secrets stay in your shell or an ignored local .env file, never in Git. +GITHUB_TOKEN= +CANTON_JSON_API_URL=http://127.0.0.1:3975 CANTON_PACKAGE_ID= - -# Three distinct Canton parties are required for the full competition proof. CANTON_MAINTAINER_PARTY= CANTON_CONTRIBUTOR_PARTY= CANTON_VERIFIER_PARTY= - -# LocalNet may use one token with rights for all demo parties. CANTON_TOKEN= - -# Or provide role-specific tokens for stronger separation. CANTON_MAINTAINER_TOKEN= CANTON_CONTRIBUTOR_TOKEN= CANTON_VERIFIER_TOKEN= - -# Optional GitHub token. Required while this repository is private. -GITHUB_TOKEN= - -# Optional full-demo overrides. +CANTON_USER_ID= +# Explicit local sandbox only. Never enable for a remote/production service. +CANTON_INSECURE_LOCAL=false COMMITLEDGER_ISSUE_URL=https://github.com/Saidur-droid/CommitLedger/issues/5 COMMITLEDGER_PR_NUMBER=6 COMMITLEDGER_CONTRIBUTOR_GITHUB=Saidur-droid -COMMITLEDGER_REWARD_AMOUNT=100 -COMMITLEDGER_BASE_BRANCH=main -COMMITLEDGER_EVIDENCE_FILE= +COMMITLEDGER_EVIDENCE_FILE=./evidence/canton-proof.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ca7dee..5127877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,54 +1,76 @@ -name: Manual Verification - +name: Verified Delivery on: + push: + branches: [main, 'fix/**'] + pull_request: workflow_dispatch: - permissions: contents: read - + issues: read + pull-requests: read +concurrency: + group: verification-${{ github.ref }} + cancel-in-progress: true jobs: - node-verifier: + node: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: "22" - - run: npm test - - daml: + node-version: '22' + - name: Run every Node test + run: | + mkdir -p evidence + set -o pipefail + npm test 2>&1 | tee evidence/node-tests.log + - uses: actions/upload-artifact@v4 + if: always() + with: + name: node-evidence-${{ github.sha }} + path: evidence/node-tests.log + canton-proof: + # Untrusted fork PRs never receive a private-repository read token. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + timeout-minutes: 25 steps: - uses: actions/checkout@v4 - - name: Install DPM SDK 3.5.12 - run: | - curl -sSL https://get.digitalasset.com/install/install.sh | DPM_EDITION=open-source bash -s 3.5.12 - echo "$HOME/.dpm/bin" >> "$GITHUB_PATH" - - name: Build Daml - working-directory: daml - run: DPM_SDK_VERSION=3.5.12 dpm build --all - - name: Test Daml - working-directory: daml - run: DPM_SDK_VERSION=3.5.12 dpm test - + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Install pinned open-source DPM + run: bash scripts/bootstrap-dpm.sh + - name: Build, test, deploy and capture a real local Canton lifecycle + env: + GITHUB_TOKEN: ${{ github.token }} + run: bash scripts/run-local-proof.sh + - uses: actions/upload-artifact@v4 + if: always() + with: + name: canton-evidence-${{ github.sha }} + path: | + evidence/*.log + evidence/*.json + if-no-files-found: warn + retention-days: 30 repository-gates: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - - name: Required competition files exist + - name: Required files and constraints run: | test -f LOCKED_DECISIONS.md test -f docs/COMPETITION_COMPLIANCE.md - test -f docs/WINNING_STANDARD.md - test -f docs/RULES_SNAPSHOT.md + test -f docs/EVIDENCE_MANIFEST.md + test -f docs/DEMO_SCRIPT.md + test -f docs/SUBMISSION_DRAFT.md test -f daml/daml/CommitLedger.daml - test -f daml/daml/Test.daml - test -f src/github-verifier.mjs - test -f src/canton-json-api.mjs - test -f web/index.html - - name: No forbidden runtime dependencies - run: | - ! grep -RniE 'vercel|supabase' package.json src web scripts daml || { - echo "Forbidden runtime dependency reference detected" - exit 1 - } + ! grep -RniE 'vercel|supabase' package.json src web scripts daml + ! git ls-files | grep -E '(^|/)\.env(\.local)?$' diff --git a/.gitignore b/.gitignore index 9b2e96c..7b6a436 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ -.DS_Store -.env -.env.* -!.env.example node_modules/ -coverage/ -dist/ +.env +.env.local +.env.*.local daml/.daml/ -*.dar -*.log +evidence/*.log +evidence/*.json +!evidence/.gitkeep +*.webm +*.mp4 diff --git a/README.md b/README.md index 4d25a02..afb66c7 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,75 @@ **Canton-native settlement infrastructure for verified open-source work.** -CommitLedger binds a real GitHub bounty issue and merged pull request to role-separated Daml authorization and an auditable Canton settlement receipt. +CommitLedger binds canonical contribution evidence to role-separated authorization and an auditable settlement receipt. -## Competition thesis +## Active competition targets — 7 October 2026 + +This sprint has exactly two active submission targets: + +1. **HackCanton Season 3** + - Submission deadline: **9 Oct 2026, 23:59 UTC** + - Bangladesh time: **10 Oct 2026, 05:59** +2. **Crypto World's Fair — Colosseum** + - Contest/submission cutoff: **12 Oct 2026, 23:59 PT** + - Bangladesh time: **13 Oct 2026, 12:59** + +The 1 October plan that retired HackCanton is superseded by the owner's 7 October instruction to submit CommitLedger to both programs. + +See [Competition Targets](docs/COMPETITION_TARGETS.md), [Submission Status](docs/SUBMISSION_STATUS_20261007.md), [Final Checklist](docs/FINAL_CHECKLIST.md), and [Submission Draft](docs/SUBMISSION_DRAFT.md). + +## Product thesis `GitHub Issue -> Bounty -> Claim -> Pull Request -> Merge Evidence -> Canton Settlement -> SettlementReceipt` -Canton is the trust and settlement layer, not a cosmetic integration. Removing Canton removes the product's authorization, provenance, replay protection and final settlement record. +Canton/Daml is the authorization and workflow-state layer, not a cosmetic integration. GitHub remains the canonical external work-evidence source. ## Core guarantees - **Maintainer** creates the bounty, accepts the claim and settles verified work. -- **Contributor** creates the claim and submits the exact PR. -- **Verifier** can attest only canonical GitHub merge evidence. -- Merge evidence includes the **exact bounty issue number**. -- Wrong issue, wrong SHA, unmerged work and unauthorized actions are rejected. -- Settlement consumes the verified contract, blocking duplicate settlement. +- **Contributor** claims work and submits the exact PR. +- **Verifier** attests canonical merge evidence. +- Merge evidence is bound to the exact issue and PR metadata. +- Wrong evidence, unauthorized actions and replay/duplicate settlement are rejected. +- Settlement consumes the verified contract and creates an auditable `SettlementReceipt`. - `DEMO_CREDIT` is explicitly non-production test value. -## Real GitHub fixture +## Frozen technical proof + +The last fully executed, source-bound technical proof is: + +`26bd992787401f6458f6685d2ab76aacd05eab4e` + +Verified at that exact code commit: + +- Node tests: **86/86 PASS** +- DPM **3.5.12** PASS +- Daml build PASS +- Daml Script tests PASS +- fresh Canton sandbox PASS +- six ledger transitions PASS +- `SettlementReceipt` generated +- wrong-evidence rejection captured +- unauthorized-settlement rejection captured +- duplicate/replay rejection captured + +External GitHub evidence fixture: + +- Repository: `Saidur-droid/MergeEarn` +- Open issue: `#69` +- Merged PR: `#73` + +**MergeEarn is only the external proof fixture. CommitLedger is the product being submitted.** -The reproducible competition fixture is: +Canonical proof service: -- Open issue: `#5` -- Merged PR: `#6` -- PR #6 references issue #5 without closing it. +https://commitledger-proof-final.onrender.com -This lets a judge repeatedly prove the issue -> merged PR trust boundary without fake users or fake traction. +Proof JSON: + +https://commitledger-proof-final.onrender.com/api/proof + +Planning/documentation commits after the frozen proof do not become runtime-proven automatically. If executable code changes, rerun the complete proof on the exact new commit. ## Run locally @@ -41,51 +83,35 @@ npm start Open `http://127.0.0.1:4173`. -Because this repository is currently private, set `GITHUB_TOKEN` with read access before using the canonical GitHub verifier. - ## Run the full Canton proof -Configure `.env.example` values for: -- Canton JSON Ledger API URL; -- package ID; -- three distinct parties; -- one LocalNet token with rights to all demo parties, or role-specific tokens. - -Then: +Configure the documented Canton environment values, then run: ```bash -npm run demo:full +bash scripts/run-local-proof.sh ``` -The runner automatically captures each Canton `updateId`, `completionOffset` and active `contractId`, executes negative authorization/replay checks, and finishes at the active `SettlementReceipt`. - -See [Judge Runbook](docs/JUDGE_RUNBOOK.md) for the exact operator path. - -## Canton / Daml +The proof path reruns tests, builds/tests Daml, starts a fresh Canton sandbox, captures each ledger transition, executes negative authorization/replay checks, validates one coherent evidence bundle, and finishes at `SettlementReceipt`. -The Daml project is under `daml/` and is pinned to the stable open-source DPM SDK bundle **3.5.12**. +## Submission assets -Verification commands: - -```bash -bash scripts/bootstrap-dpm.sh -bash scripts/verify-all.sh -``` - -## Competition governance - -- [Locked decisions](LOCKED_DECISIONS.md) -- [Competition compliance](docs/COMPETITION_COMPLIANCE.md) -- [Winning standard](docs/WINNING_STANDARD.md) -- [Rules snapshot](docs/RULES_SNAPSHOT.md) -- [Architecture](docs/ARCHITECTURE.md) -- [Threat model](docs/THREAT_MODEL.md) -- [Judge runbook](docs/JUDGE_RUNBOOK.md) -- [Evidence manifest](docs/EVIDENCE_MANIFEST.md) -- [Build status](docs/BUILD_STATUS.md) +- [Rules recheck — 7 Oct](docs/RULES_RECHECK_20261007.md) +- [Submission status — 7 Oct](docs/SUBMISSION_STATUS_20261007.md) +- [Competition targets](docs/COMPETITION_TARGETS.md) +- [Final checklist](docs/FINAL_CHECKLIST.md) +- [Submission master draft](docs/SUBMISSION_DRAFT.md) +- [HackCanton pitch](docs/PITCH.md) +- [HackCanton demo script](docs/DEMO_SCRIPT.md) +- [Colosseum pitch script](docs/COLOSSEUM_PITCH_SCRIPT.md) +- [Colosseum demo script](docs/COLOSSEUM_DEMO_SCRIPT.md) +- [Hackathon work disclosure](docs/HACKATHON_WORK_DISCLOSURE.md) +- [AI assistance disclosure](docs/AI_DISCLOSURE.md) +- [Delivery status](docs/DELIVERY_STATUS.md) ## Integrity -CommitLedger does not claim Canton Coin transfer, fiat settlement, external adoption, mainnet usage, green CI, or runtime Canton success unless that evidence has actually been produced. +CommitLedger does not claim Canton Coin transfer, fiat settlement, external adoption, MainNet usage, revenue, paying customers, or production custody without evidence. + +`DEMO_CREDIT` is test value only. Canton is a registered trademark of Digital Asset (Switzerland) GmbH. CommitLedger is an independent project and is not sponsored or endorsed by Digital Asset. diff --git a/daml/daml/CommitLedger.daml b/daml/daml/CommitLedger.daml index 6c0721b..3c0a21e 100644 --- a/daml/daml/CommitLedger.daml +++ b/daml/daml/CommitLedger.daml @@ -1,5 +1,7 @@ module CommitLedger where +import qualified DA.Text as Text + -- CommitLedger intentionally models a demo/test settlement asset. -- It does not claim to transfer fiat, Canton Coin, or production funds. @@ -19,6 +21,7 @@ data MergeEvidence = MergeEvidence prNumber : Int prUrl : Text headSha : Text + mergeCommitSha : Text baseBranch : Text merged : Bool mergedAt : Text @@ -175,6 +178,7 @@ template SubmittedBounty assertMsg "evidence URL mismatch" (evidence.prUrl == pullRequest.prUrl) assertMsg "evidence head SHA mismatch" (evidence.headSha == pullRequest.headSha) assertMsg "evidence base branch mismatch" (evidence.baseBranch == pullRequest.baseBranch) + assertMsg "merge commit SHA must have 40 characters" (Text.length evidence.mergeCommitSha == 40) assertMsg "merge timestamp is required" (evidence.mergedAt /= "") assertMsg "evidence hash is required" (evidence.evidenceHash /= "") create VerifiedBounty with @@ -229,6 +233,7 @@ template VerifiedBounty pullRequest rewardAmount rewardUnit + mergeCommitSha = evidence.mergeCommitSha evidenceHash = evidence.evidenceHash settledAt settlementRef @@ -245,6 +250,7 @@ template SettlementReceipt pullRequest : GitHubPullRequestRef rewardAmount : Decimal rewardUnit : Text + mergeCommitSha : Text evidenceHash : Text settledAt : Time settlementRef : Text diff --git a/daml/daml/Test.daml b/daml/daml/Test.daml index 4106d40..93ba041 100644 --- a/daml/daml/Test.daml +++ b/daml/daml/Test.daml @@ -49,6 +49,7 @@ mkEvidence = MergeEvidence with prUrl = "https://github.com/Saidur-droid/CommitLedger/pull/7" headSha = "abc123" baseBranch = "main" + mergeCommitSha = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" merged = True mergedAt = "2026-09-27T00:00:00Z" evidenceHash = "sha256:demo-evidence" @@ -87,6 +88,7 @@ testHappyPath = do receipt.bountyId === "bounty-001" receipt.rewardAmount === 100.0 receipt.rewardUnit === "DEMO_CREDIT" + receipt.mergeCommitSha === "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" receipt.evidenceHash === "sha256:demo-evidence" receipt.settlementRef === "demo-settlement-001" @@ -128,6 +130,9 @@ testBadEvidenceFails = do submitMustFail verifier do exerciseCmd submittedCid SubmittedBounty_VerifyMerged with evidence = mkEvidence with issueNumber = 999 + submitMustFail verifier do + exerciseCmd submittedCid SubmittedBounty_VerifyMerged with + evidence = mkEvidence with mergeCommitSha = "" testSettlementAuthorizationAndReplayProtection : Script () testSettlementAuthorizationAndReplayProtection = do diff --git a/docs/AI_DISCLOSURE.md b/docs/AI_DISCLOSURE.md new file mode 100644 index 0000000..50ce481 --- /dev/null +++ b/docs/AI_DISCLOSURE.md @@ -0,0 +1,7 @@ +# AI Assistance Disclosure + +CommitLedger was developed with AI-assisted tooling for research, planning, code review, test generation, documentation, and implementation support. + +All submitted code, contracts, tests, security claims, and competition materials remain the responsibility of the participating team. AI-generated or AI-assisted output is not treated as evidence by itself; runtime and test claims require executed proof. + +No AI tool is represented as a team member, customer, validator, or organizer. diff --git a/docs/AUTOMATED_RUNBOOK.md b/docs/AUTOMATED_RUNBOOK.md new file mode 100644 index 0000000..60e5aa2 --- /dev/null +++ b/docs/AUTOMATED_RUNBOOK.md @@ -0,0 +1,113 @@ +# Automated verification and real Canton proof + +## Preferred execution + +Use GitHub Actions when hosted runners allocate correctly. If hosted Actions is unavailable, use the repository Codespace/devcontainer. Remote Desktop is optional. + +The proof runner is designed as a one-command path: + +```bash +bash scripts/run-local-proof.sh +``` + +It now: +- binds evidence to the exact checked-out Git commit; +- reruns the full Node suite; +- compiles/tests Daml with DPM 3.5.12; +- extracts the package identifier from the fresh DAR; +- chooses a free loopback JSON API port for each run, avoiding stale-port collisions; +- starts a fresh Canton sandbox with the DPM 3.5.12-compatible `--canton-port-file` ready signal; +- waits for both Canton readiness and the JSON ledger endpoint; +- retries only the known transient `PARTY_ALLOCATION_WITHOUT_CONNECTED_SYNCHRONIZER` race; +- allocates three parties and a demo user; +- obtains a Codespaces/GitHub CLI token automatically when `GITHUB_TOKEN` is not already present; +- runs the real GitHub-to-ledger lifecycle; +- captures structured negative rejections; +- rejects mixed-commit evidence bundles. + +## Prerequisites + +Use an internet-connected Linux environment with Node.js 22+, Java 21, Python 3, curl and Bash. The repository devcontainer supplies these dependencies and installs the pinned DPM bundle. + +Do not paste credentials into chat or commit them. + +## Commands + +For a new Codespace, bootstrap is normally automatic. If needed: + +```bash +bash scripts/bootstrap-codespace.sh +``` + +Then run the proof: + +```bash +bash scripts/run-local-proof.sh +``` + +The script stops on the first unproven condition and prints the relevant runtime log tail. + +## Keep a verified session open for the judge + +```bash +COMMITLEDGER_SERVE_AFTER_PROOF=true bash scripts/run-local-proof.sh +``` + +Only after proof validation succeeds does the UI stay open. In Codespaces, open forwarded port 4173 from the Ports panel. + +Each UI run creates a new demo bounty. DEMO_CREDIT remains non-production test value. + +## Evidence + +Preserve these artifacts from the same run: +- `node-tests.log` +- `daml-build.log` +- `daml-tests.log` +- `verification.json` +- `canton-ports.json` +- `ledger-setup.json` +- `canton-proof.json` +- `canton-demo.log` +- `canton-sandbox.log` + +`verification.json.commit` and `canton-proof.json.sourceCommit` must match exactly. + +A timeout, network error, authentication error, rate limit, malformed command, mocked transport, or CI runner failure is never accepted as Daml authorization/security evidence. + +## Existing authenticated ledger + +For a real authenticated development ledger, export the values in `.env.example` and run: + +```bash +export COMMITLEDGER_EVIDENCE_FILE=./evidence/canton-proof.json +npm run demo:full +npm start +``` + +The application does not automatically source environment files. + +## GitHub Actions + +The workflow contains Node, Canton proof and repository gate jobs. If GitHub assigns a hosted runner, no personal computer or Codespace is required. + +The previously observed `steps: []` + `runner_id: 0` failure occurs before application steps execute and therefore is not a code-test result. + +## Browser/video + +After a verified runtime is open: + +```bash +python3 -m pip install playwright +python3 -m playwright install chromium +python3 scripts/record-demo.py +``` + +The recorder retains a technical capture only when the live lifecycle, receipt and negative-proof checks succeed. The final submission video must remain ≤5 minutes and be reviewed before publishing. + +## Technical references + +- https://docs.digitalasset.com/build/3.4/component-howtos/application-development/dpm-sandbox.html +- https://docs.digitalasset.com/build/3.5/reference/json-api/openapi.html +- https://archived.docs.digitalasset.com/operate/3.5/reference/error_codes.html + +The pinned DPM SDK remains 3.5.12. Only an executed proof establishes final compatibility. diff --git a/docs/BUILD_STATUS.md b/docs/BUILD_STATUS.md index e6c79e6..60deda7 100644 --- a/docs/BUILD_STATUS.md +++ b/docs/BUILD_STATUS.md @@ -1,77 +1,7 @@ # Build Status -_Last updated: 2026-09-27._ +Last reviewed: 2026-09-29. -## Implemented +See [Delivery status](DELIVERY_STATUS.md) for the current evidence-based status, [Automated runbook](AUTOMATED_RUNBOOK.md) for execution, and [Final checklist](FINAL_CHECKLIST.md) for release/submission gates. -- Two locked goals remain the governing gates: verified competition compliance and #1-caliber judge quality. -- Role-separated Daml lifecycle: Maintainer -> Contributor -> Verifier -> Maintainer receipt. -- Explicit revision / resubmission path. -- Evidence binding across repository, source issue number, PR number, PR URL, head SHA, base branch and merge timestamp. -- Daml rejection cases for wrong issue, wrong SHA, unmerged work, unauthorized verification, unauthorized settlement and replay. -- Canton JSON Ledger API v2 client with active-contract discovery. -- Full lifecycle orchestrator that automatically carries real contract IDs from stage to stage. -- Runtime negative checks for wrong-issue evidence, unauthorized settlement and duplicate settlement. -- Final active `SettlementReceipt` discovery and proof bundle. -- Local judge UI with real update-ID / contract-ID timeline and receipt view. -- One-command CLI: `npm run demo:full`. -- Real GitHub fixture: open issue #5 + merged PR #6 that references #5 without closing it. -- Free/local-first runtime configuration; no Vercel, Supabase or paid API dependency. - -## Verified external fixture - -As of 2026-09-27: -- issue #5 is open; -- PR #6 is merged; -- PR #6 body references issue #5; -- repository, PR and issue all belong to `Saidur-droid/CommitLedger`. - -This fixture is competition/demo evidence, not user traction. - -## Toolchain baseline - -CommitLedger pins the stable open-source DPM SDK bundle **3.5.12**. - -The current upstream 3.5 assembly manifest used during this build reports: -- SDK bundle: 3.5.12; -- Canton open source: 3.5.19; -- damlc: 3.5.3; -- Daml Script: 3.5.3. - -## Test status - -An earlier Node verifier baseline was executed successfully (3 passed, 0 failed). - -Since then the repository test suite has been expanded substantially to cover: -- domain transitions and distinct role enforcement; -- issue-bound GitHub evidence; -- Canton command builders; -- active-contract parsing; -- runtime configuration. - -Do **not** describe the expanded suite as green until `npm test` is rerun in an execution environment with the current branch. - -## Runtime evidence gate - -The current ChatGPT sandbox cannot install the official DPM bundle from the Digital Asset distribution endpoint, and the private repository's GitHub-hosted Actions jobs have previously failed before runner steps execute. - -Therefore these are intentionally not marked verified yet: -- current Daml compile success; -- current Daml Script test success; -- DAR deployment to Canton LocalNet; -- real full lifecycle execution through `SettlementReceipt`; -- captured runtime update IDs / contract IDs; -- captured runtime negative-check failures; -- final demo recording. - -The repository contains the commands required to produce those artifacts on an internet-connected Canton development environment: - -```bash -bash scripts/bootstrap-dpm.sh -bash scripts/verify-all.sh -npm run demo:full -``` - -## Submission rule - -CommitLedger may only be called **final submission ready** after every runtime item in `docs/EVIDENCE_MANIFEST.md` has real captured evidence. +Original Node suite: 16 passed. Expanded delivery Node suite: 65 passed. Daml build/tests, real Canton lifecycle evidence, full browser QA, final video, and official portal recheck remain unverified. CI configuration is not CI success. Do not claim 100% or submission-ready before those gates pass. diff --git a/docs/CLOUD_EXECUTION.md b/docs/CLOUD_EXECUTION.md new file mode 100644 index 0000000..fb449f1 --- /dev/null +++ b/docs/CLOUD_EXECUTION.md @@ -0,0 +1,64 @@ +# Cloud execution — no always-on local PC required + +CommitLedger does **not** fundamentally depend on Remote Desktop Commander. + +A real Canton/Daml proof only needs a Linux-capable environment that can: +- run Node.js 22; +- run Java 21; +- install the pinned DPM bundle; +- access GitHub and Digital Asset download endpoints; +- keep local ports available while the proof runs. + +## Preferred automation order + +### 1. GitHub Actions — zero-touch target + +This is the preferred final state. A push/PR should execute Node tests, Daml build/tests, a fresh Canton sandbox, evidence capture and repository gates automatically. + +At the moment the repository's hosted jobs are failing before a runner is allocated (`steps: []`, `runner_id: 0`). That is an infrastructure/account-level blocker until GitHub actually assigns a runner. Do not treat it as a failing test. + +When hosted Actions begins allocating runners, no personal computer or Remote Desktop session is needed. + +### 2. GitHub Codespaces — easiest manual fallback + +The repository contains `.devcontainer/devcontainer.json`. + +Open the repository in a Codespace and let the container bootstrap. It installs: +- Node 22; +- Java 21; +- Python 3; +- pinned DPM via `scripts/bootstrap-dpm.sh`; +- then runs the Node test suite. + +After creation, run: + +```bash +export GITHUB_TOKEN=... +bash scripts/run-local-proof.sh +``` + +If the repository becomes public and the fixture is public, the GitHub token requirement may be removable depending on GitHub API/rate-limit needs. + +To keep the judge UI open after a successful proof: + +```bash +COMMITLEDGER_SERVE_AFTER_PROOF=true bash scripts/run-local-proof.sh +``` + +Codespaces usage/availability depends on the GitHub account and its configured quota/billing. CommitLedger does not change billing settings automatically. + +### 3. Remote Desktop Commander — optional fallback + +Use only when a local development machine is convenient. It is useful because the agent can execute commands and inspect the running browser, but it is not an architectural dependency. + +The device must be online only while the agent is actively executing local commands. It does not need to remain connected after evidence has been captured and retained. + +## One coherent proof rule + +Regardless of environment, the final evidence must come from: +- one source commit; +- one run ID; +- one real Canton runtime; +- retained Node/Daml/runtime logs. + +Never combine a Node log from one commit with a Canton proof from another and mark the result green. diff --git a/docs/COLOSSEUM_DEMO_SCRIPT.md b/docs/COLOSSEUM_DEMO_SCRIPT.md new file mode 100644 index 0000000..4feb696 --- /dev/null +++ b/docs/COLOSSEUM_DEMO_SCRIPT.md @@ -0,0 +1,68 @@ +# Colosseum Product Demo Script — CommitLedger + +Target: **2:15–2:45**, under the current <=3 minute FAQ limit. + +## 0:00–0:15 — Start on proof summary + +Show: +- product name; +- frozen proof commit; +- runtime verified state. + +Say: +“CommitLedger turns canonical GitHub contribution evidence into a role-authorized Canton settlement receipt.” + +## 0:15–0:40 — External work evidence + +Show the real evidence fixture: +- repository `Saidur-droid/MergeEarn`; +- open issue `#69`; +- merged PR `#73`. + +Say: +“This is a real GitHub evidence fixture, not a traction claim. CommitLedger verifies the canonical issue and merged pull request before settlement can advance.” + +## 0:40–1:35 — Six-step lifecycle + +Show the workflow: +1. Bounty created. +2. Claim created. +3. Claim accepted. +4. Pull request submitted. +5. Merge evidence verified. +6. Work settled. + +Expand at least one real `updateId` and `contractId`. + +End on the `SettlementReceipt`. + +## 1:35–2:15 — Security proof + +Show the three rejection cases: +- wrong issue/evidence; +- unauthorized settlement attempt; +- duplicate/replay settlement. + +Say: +“These are application-level authorization and evidence failures captured from the proven run, not network errors presented as security proof.” + +## 2:15–2:35 — Architecture + +Show the boundary: +- GitHub = external work evidence; +- verifier = attestation boundary; +- Daml = allowed state transitions; +- Canton = workflow state and receipt. + +## 2:35–2:45 — Close + +“CommitLedger makes settlement decisions inspectable, role-aware and replay-safe.” + +## Before upload + +- keep <=3:00; +- English only; +- hide tokens/secrets; +- ensure every displayed proof element belongs to the frozen proof commit; +- do not imply DEMO_CREDIT is money; +- test video and proof links as a logged-out judge. diff --git a/docs/COLOSSEUM_PITCH_SCRIPT.md b/docs/COLOSSEUM_PITCH_SCRIPT.md new file mode 100644 index 0000000..dc9ae52 --- /dev/null +++ b/docs/COLOSSEUM_PITCH_SCRIPT.md @@ -0,0 +1,34 @@ +# Colosseum Presentation Script — CommitLedger + +Target: **about 2 minutes**, comfortably inside the current 2–3 minute FAQ guidance. + +## 0:00–0:15 — Hook + +“Open-source programs can prove that a pull request merged. What they often cannot prove is who was authorized to verify the work, who was authorized to settle it, and whether the same contribution was paid twice. CommitLedger is the control layer between merged work and authorized settlement.” + +## 0:15–0:40 — Product + +“CommitLedger binds a canonical GitHub issue and merged pull request to a role-authorized workflow: Bounty, Claim, Pull Request, Verification, Settlement, and a final SettlementReceipt. The maintainer, contributor, and verifier have different permissions, and the workflow only advances when the evidence and authorization are correct.” + +## 0:40–1:05 — Why blockchain / why Canton + +“GitHub is the source of truth for the work event. Canton and Daml are the source of truth for settlement workflow state. Daml defines the allowed transitions, consumed contracts block replay, and the ledger gives us inspectable contract and update identifiers. If you remove the ledger layer, the core authorization and replay protection disappear.” + +## 1:05–1:30 — Proof + +“We executed the full workflow on a fresh Canton sandbox. The Node suite passed 86 out of 86 tests, Daml built and tested successfully, all six ledger transitions completed, and the final SettlementReceipt was generated. We also captured three negative proofs: wrong evidence, unauthorized settlement, and duplicate settlement.” + +## 1:30–1:50 — Market + +“Our first customers are organizations running contributor and bounty programs: open-source companies, foundations, ecosystem funds, and engineering organizations. The wedge is one repository and one bounty campaign, then expand into organization policy, audit controls, and approved production settlement adapters.” + +## 1:50–2:00 — Close + +“CommitLedger separates evidence from authority: GitHub proves what happened; the verifier attests it; and Canton controls what each party is allowed to do next.” + +## Recording rules + +- English only. +- Do not claim revenue, customers, MainNet, Canton Coin or fiat settlement. +- Mention that pre-existing development is disclosed in the submission. +- Keep the product and founder reasoning clear; avoid a long technical walkthrough here. diff --git a/docs/COMPETITION_TARGETS.md b/docs/COMPETITION_TARGETS.md new file mode 100644 index 0000000..a9a5cfc --- /dev/null +++ b/docs/COMPETITION_TARGETS.md @@ -0,0 +1,181 @@ +# Competition Targets — 7 October 2026 + +**Canonical plan for the current CommitLedger sprint.** + +The owner's 7 October instruction supersedes the 1 October planning note that had retired HackCanton. + +## Active targets + +1. **HackCanton Season 3** +2. **Crypto World's Fair — Colosseum** + +Ideathon Bangladesh 2026 is not part of this immediate CommitLedger submission sprint. + +## Frozen technical baseline + +Last fully executed, source-bound technical proof: + +`26bd992787401f6458f6685d2ab76aacd05eab4e` + +Canonical proof service: + +https://commitledger-proof-final.onrender.com + +Proof endpoint: + +https://commitledger-proof-final.onrender.com/api/proof + +Verified on the frozen code commit: +- Node 86/86 PASS; +- DPM 3.5.12 PASS; +- Daml build PASS; +- Daml Script tests PASS; +- fresh Canton sandbox; +- six ledger transitions; +- SettlementReceipt; +- wrong-evidence rejection; +- unauthorized-settlement rejection; +- duplicate/replay rejection; +- exact source-commit binding. + +Planning/documentation commits after the frozen proof are not automatically runtime-proven. If executable code changes, rerun the full proof. + +--- + +# Target 1 — HackCanton Season 3 + +## Live-verified timing + +Current public schedule: +- build phase: 18 Sep–9 Oct 2026; +- submission deadline: **9 Oct 2026, 23:59 UTC**; +- Bangladesh equivalent: **10 Oct 2026, 05:59**; +- judging: 10–18 Oct; +- finalists: 19 Oct; +- Grand Final: 21 Oct, 14:00 UTC. + +Primary sources/recheck pointers: +- https://appsfactory.cc/hackathons +- https://forum.canton.network/ +- current ecosystem schedule/article notes captured in `RULES_RECHECK_20261007.md`. + +## Fit + +CommitLedger is a strong Canton-native business-workflow submission because Daml/Canton controls the core authorization and settlement state: + +`Issue -> Bounty -> Claim -> PR -> Verify -> Settle -> SettlementReceipt` + +Primary track candidate: **Real-World Asset & Business Workflows** because the product is an end-to-end role-authorized business workflow. Verify the actual portal track names before final selection. + +## Technical status + +**GREEN.** Product engineering and runtime proof are complete at the frozen proof commit. + +## Submission-side status + +Ready now: +- product; +- runtime proof; +- security/negative proof; +- architecture/business brief; +- pitch text; +- demo storyboard; +- AI disclosure; +- hackathon-work disclosure draft. + +Still external/manual: +- AppsFactory login/registration state; +- team/project profile state; +- actual portal-required track and fields; +- recorded final video; +- repository judge access/public visibility; +- final portal submission; +- submission receipt. + +### Historical AppsFactory gate warning + +This repository previously modeled external gates named: +- `mana1000`; +- `activity10Days`; +- `journalNonEmpty`; +- `projectProfileComplete`; +- `track1Selected`. + +These are **not being promoted here as universal public rules** because the live public site does not expose the full private participant checklist. Inspect the logged-in AppsFactory portal immediately. If any of these are mandatory and already unmet, do not misrepresent eligibility. + +--- + +# Target 2 — Crypto World's Fair — Colosseum + +## Official timing + +Official rules state: +- contest starts: **14 Sep 2026, 6:00 AM PT**; +- contest ends / registration + submission cutoff: **12 Oct 2026, 11:59 PM PT**; +- Bangladesh equivalent: **13 Oct 2026, 12:59**; +- winners announced by 5 Dec 2026. + +Official page: +https://colosseum.com/worldsfair + +Official rules: +https://colosseum.com/legal/Crypto%20World%27s%20Fair%20Hackathon%20Rules.pdf + +## Eligibility/rules currently verified + +- no purchase necessary; +- age of majority in country/residence or at least 18, whichever is older; +- Bangladesh is not in the current listed excluded jurisdictions; +- every member must register before the cutoff; +- one entrant may be on only one team; +- a team may submit one project at a time; +- all submitted content must be in English; +- judging includes functionality, potential impact, novelty, UX, open-source/composability, and business plan. + +Current Colosseum FAQ also states: +- pre-existing code is allowed; +- previous development must be disclosed; +- judging focuses on work completed during the contest; +- repository link is required (private allowed if review access is granted); +- presentation video is requested; +- product demo video is requested; +- GTM, demand validation and distribution plan are requested. + +## Fit + +Submit CommitLedger as **verifiable work-to-settlement infrastructure for open-source ecosystems and contributor programs**. + +Do not force an ecosystem-track claim unless CommitLedger genuinely integrates the required chain. The general competition remains available across ecosystems. + +## Technical status + +**GREEN.** Reuse the frozen proven product; do not rewrite core code just to make it look new. + +## Competition-specific work still needed + +- Colosseum account/join confirmation; +- project record in portal; +- transparent pre-existing-work disclosure; +- concise founder/market narrative; +- demand validation that is real, not invented; +- GTM/business model; +- 2–3 minute presentation video; +- <=3 minute product demo video; +- repository review access; +- final submission + receipt. + +--- + +# Execution order — 7 October + +1. **HackCanton portal check first** because its hard deadline is earlier. +2. Resolve registration/profile/eligibility gates. +3. Record and upload HackCanton final video. +4. Ensure judge-accessible repo + proof links. +5. Submit HackCanton and save receipt. +6. Then complete Colosseum registration/project fields. +7. Record Colosseum presentation + demo videos. +8. Finalize pre-existing-work disclosure, GTM, validation and business model. +9. Submit Colosseum and save receipt. + +Owner constraint: use free tools/services unless explicitly changed. diff --git a/docs/DELIVERY_STATUS.md b/docs/DELIVERY_STATUS.md new file mode 100644 index 0000000..b1bfd30 --- /dev/null +++ b/docs/DELIVERY_STATUS.md @@ -0,0 +1,79 @@ +# Delivery Status — 7 October 2026 + +Delivery branch: `fix/verified-delivery-20260929`. + +## Product / technical status + +**Technical delivery is GREEN.** + +Frozen fully executed proof commit: + +`26bd992787401f6458f6685d2ab76aacd05eab4e` + +Canonical Render proof service: + +`commitledger-proof-final` + +Live page: +https://commitledger-proof-final.onrender.com + +Proof endpoint: +https://commitledger-proof-final.onrender.com/api/proof + +Successful proven deploy recorded in project history: +`dep-daujs4m0tbcc73dn0c80` + +Verified on the frozen code state: +- Node 86/86 PASS; +- DPM 3.5.12 PASS; +- Daml build PASS; +- Daml Script tests PASS; +- Java 21 runtime; +- fresh Canton sandbox; +- DAR/package identity extraction; +- three distinct demo parties; +- external GitHub fixture `Saidur-droid/MergeEarn#69 -> PR #73`; +- six real Canton transitions; +- SettlementReceipt; +- wrong-evidence rejection; +- unauthorized-settlement rejection; +- duplicate/replay rejection; +- source-commit binding. + +## Active competition direction + +The current sprint has exactly two active targets: + +1. **HackCanton Season 3** +2. **Crypto World's Fair — Colosseum** + +The 1 October retirement of HackCanton is superseded by the owner's 7 October instruction. + +## Submission readiness + +### HackCanton +- technical product: DONE; +- evidence/security proof: DONE; +- written pitch/demo plan: DONE; +- rules deadline recheck: DONE; +- logged-in AppsFactory eligibility/profile state: **PENDING MANUAL CHECK**; +- recorded final video: PENDING; +- repo/publication gate: PENDING; +- final portal submission/receipt: PENDING. + +### Colosseum +- technical product: DONE; +- official rules recheck: DONE; +- pre-existing-work approach: DONE; +- submission copy: DRAFTED; +- presentation/demo scripts: DONE; +- portal registration/join state: **PENDING MANUAL CHECK**; +- real demand validation: PENDING if available; +- final videos: PENDING; +- final portal submission/receipt: PENDING. + +## Engineering state + +Do not reopen product engineering unless a verified rule requires it or a real defect appears. + +Documentation-only commits may advance the branch without changing the frozen technical proof claim. If executable code changes, rerun the full proof on the exact new commit. diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md new file mode 100644 index 0000000..5731c54 --- /dev/null +++ b/docs/DEMO_SCRIPT.md @@ -0,0 +1,75 @@ +# Demo script — evidence first + +**Official Season 3 limit: 5 minutes maximum.** +Target runtime: **3:30–4:15**, leaving safety margin. + +Status: script prepared; actual verified-runtime video NOT yet recorded. + +## Preparation + +Run the automated proof on the exact delivery commit. Do not film a simulated lifecycle or reuse IDs from another commit/run. Keep the successful Canton session open. Hide tokens, terminals containing secrets and unrelated private information. + +## Suggested timeline + +### 0:00–0:25 — Problem +"A merged pull request proves that code was accepted, but it does not by itself provide a multi-party settlement workflow. CommitLedger connects canonical GitHub evidence to role-authorized Daml transitions on Canton." + +Show the top proof summary: +**Source / Authorization / Settlement / Runtime**. + +### 0:25–0:55 — Real source +Show GitHub issue #5 and merged PR #6, including the explicit issue reference and canonical merge commit SHA. + +State clearly: +- this is a reproducible demo fixture; +- it is not external traction; +- issue-reference validation does not claim semantic proof that the code solved the issue. + +### 0:55–1:20 — Roles +Show Maintainer, Contributor and Verifier. + +Explain: +- Maintainer creates/accepts/settles; +- Contributor claims/submits; +- Verifier attests canonical merge evidence; +- Canton/Daml controls authorized transitions. + +DEMO_CREDIT is test value, not money or Canton Coin. + +### 1:20–2:30 — Live lifecycle +Click **Run live Canton lifecycle** only after health reports the real ledger reachable. + +Show: +Issue → Bounty → Claim → PR → Verify → Settle → Receipt. + +Expand at least one complete update ID and contract ID. Show the final SettlementReceipt, merge commit SHA and evidence hash. Export the proof JSON. + +### 2:30–3:15 — Negative security proof +Show all three exact rejections: +1. wrong issue evidence; +2. contributor attempting settlement; +3. replay/duplicate settlement. + +Explain the actual Daml/Canton code/assertion. A network error, 401/403, timeout or rate-limit response is never presented as authorization proof. + +### 3:15–3:45 — Why Canton +"GitHub supplies external work state. The verifier is the evidence oracle. Daml controls the permitted state transitions. Canton provides the inspectable workflow state and final receipt." + +### 3:45–4:15 — Track 1 / business close +Primary users: maintainers, foundations and bounty programs. + +Pilot: +one repository → one ecosystem/foundation program → production settlement adapter after validation. + +End on the receipt/proof summary, not a marketing screen. + +## Before publishing + +- confirm every displayed ID belongs to the final source commit/run; +- remove secrets and unrelated private information; +- verify audio and legibility; +- keep total length ≤5:00; +- verify judge-accessible links in a logged-out/private browser; +- retain the unedited technical capture and evidence bundle. + +Do not present this storyboard as an already recorded or accepted submission. diff --git a/docs/EVIDENCE_MANIFEST.md b/docs/EVIDENCE_MANIFEST.md index 0bd81c4..219ed18 100644 --- a/docs/EVIDENCE_MANIFEST.md +++ b/docs/EVIDENCE_MANIFEST.md @@ -1,35 +1,69 @@ # Evidence Manifest -## Already created +Evidence is accepted only when it is tied to the exact source commit and, for runtime artifacts, one coherent run ID. -- Real open GitHub issue #5. +## Established source / test evidence + +- Real GitHub issue #5. - Real merged GitHub pull request #6. - PR #6 references issue #5 without closing it, enabling reproducible bounty-source verification. -- Daml contract source implementing the role-separated authorization lifecycle. -- Daml revision/resubmission path. -- Daml negative-test source for unauthorized verification, bad merge evidence, wrong issue binding, unauthorized settlement, and replay. -- Canonical GitHub verifier with exact issue-reference validation. -- SHA-256 merge evidence that includes repository, issue, PR, URL, head SHA, base branch and merge timestamp. -- Zero-dependency Node domain / verifier / Canton command / orchestrator tests. -- Canton JSON Ledger API client with active-contract discovery. -- One-command full lifecycle orchestrator. -- Judge-facing web application with a real Canton proof timeline and final receipt view. -- Competition compliance and winning-standard gates. -- No paid hosting/API dependency. - -## Runtime evidence still required before final submission - -These cannot be truthfully marked complete until executed against a Canton/Daml runtime: - -- Daml compiler/build success. -- Daml Script test success. -- Canton package deployment. -- Real JSON Ledger API contract creation. -- End-to-end transitions through `SettlementReceipt`. -- Captured ledger update IDs / contract IDs. -- Captured authorization/replay failures. -- Recorded final demo video based on verified runtime. - -## Integrity note - -A missing runtime proof is a build task, not something to hide with screenshots, mock transaction IDs, fabricated users, or fake adoption. +- Daml role-separated settlement lifecycle source. +- Canonical GitHub verifier with repository-qualified issue binding, merge commit SHA validation and target-branch reachability checks. +- Structured ledger rejection classifier that excludes infrastructure/auth/throttling failures from security proof. +- Competition-readiness engine with PASS / BLOCKED / FAIL semantics and mixed-run/source-commit rejection. +- Track 1 business brief, pilot plan, AI disclosure and hackathon-period work disclosure. +- Web judge UI exposes official judging criteria and fail-closed readiness state. +- Figma desktop/mobile judge-flow design. +- Linear evidence tracker with compliance/runtime/UX/submission milestones. + +Historical executed test evidence: +- original Node suite: 16/16; +- expanded Node suite on commit `202693cbc10d835166fe3bf6240fa56e529ff1cd`: 65/65; +- readiness evaluator isolated TDD fixture: 4/4; +- judge-UI isolated TDD fixture: 3/3. + +The newest branch head still requires one clean full `npm test` run. + +## Runtime evidence required before product-ready + +All items below must be generated from one current source commit/run: + +- Node full-suite log. +- DPM 3.5.12 installation/version evidence. +- Daml compiler/build log. +- Daml Script test log. +- DAR/package identifier. +- Canton runtime/environment identifier. +- Distinct Maintainer / Contributor / Verifier parties. +- Real contract and update identifiers for the lifecycle. +- Final active `SettlementReceipt`. +- Canonical merge commit SHA and evidence hash in that receipt. +- Specific Daml/Canton rejection for wrong evidence. +- Specific authorization rejection for unauthorized settlement. +- Specific inactive/not-found rejection for duplicate settlement. +- Proof JSON containing run ID and source commit. +- Browser desktop/mobile QA against the real running app. +- Final recorded demo based on the retained proof. + +A timeout, connection error, HTTP 401/403, rate limit, parser error, CI runner failure, mocked transport or static screenshot is not runtime security evidence. + +## Competition submission evidence required later + +The owner intentionally deferred these final-phase actions: +- public repository; +- public demo/video/pitch/project page; +- logged-out/private-window link verification; +- AppsFactory 1,000 Mana; +- required ≥10 activity days; +- non-empty journal; +- complete project profile; +- eligibility/team confirmation; +- final portal submission receipt. + +## GitHub Actions note + +Workflow run `36580546442` for head `3c420168453ac68f9dd1bde57e63106e29aaaeb9` failed with zero recorded steps and no allocated runner (`runner_id: 0`) in all three jobs. It produced no application test/runtime evidence and must not be treated as a code-test result. + +## Integrity rule + +Missing evidence remains BLOCKED. Never replace it with fabricated transaction IDs, fake users, fake adoption, fake security rejections, or inferred runtime success. diff --git a/docs/FINAL_CHECKLIST.md b/docs/FINAL_CHECKLIST.md new file mode 100644 index 0000000..4b8c23e --- /dev/null +++ b/docs/FINAL_CHECKLIST.md @@ -0,0 +1,99 @@ +# CommitLedger — HackCanton + Colosseum Final Checklist + +_Last updated: 7 October 2026._ + +## Shared frozen technical baseline + +- [x] Product core implemented. +- [x] Canonical GitHub evidence verification implemented. +- [x] Role-separated Maintainer / Contributor / Verifier workflow implemented. +- [x] Node tests: 86/86 PASS on frozen proof commit. +- [x] DPM 3.5.12 PASS. +- [x] Daml build PASS. +- [x] Daml Script tests PASS. +- [x] Fresh Canton sandbox proof completed. +- [x] Six-step ledger lifecycle completed. +- [x] SettlementReceipt generated. +- [x] Wrong-evidence rejection captured. +- [x] Unauthorized-settlement rejection captured. +- [x] Duplicate/replay rejection captured. +- [x] Public read-only proof service recorded. +- [x] Frozen proof commit: `26bd992787401f6458f6685d2ab76aacd05eab4e`. +- [x] AI assistance disclosure exists. +- [x] Hackathon work disclosure exists. +- [x] HackCanton pitch copy exists. +- [x] HackCanton demo script exists. +- [x] Colosseum pitch script exists. +- [x] Colosseum demo script exists. + +If executable code changes, rerun the complete proof on the exact new code commit. + +## HackCanton Season 3 + +Deadline: **9 Oct 2026, 23:59 UTC = 10 Oct 2026, 05:59 Bangladesh time**. + +### Rules / eligibility +- [x] Public deadline rechecked on 7 Oct. +- [x] Solo/team format publicly supported. +- [x] Canton-native fit documented. +- [ ] Logged-in AppsFactory registration state confirmed. +- [ ] Team/project profile completeness confirmed. +- [ ] Exact portal track selected and confirmed. +- [ ] Any private portal activity/Mana/journal requirements checked. +- [ ] Eligibility status confirmed from the participant portal. + +### Submission package +- [x] Product description drafted. +- [x] Canton integration explanation drafted. +- [x] Architecture/security evidence ready. +- [x] Demo storyboard ready. +- [x] <=5-minute target demo script ready. +- [ ] Final HackCanton video recorded. +- [ ] Video reviewed for secrets, legibility and audio. +- [ ] Repository accessible to judges. +- [ ] Live proof link tested in logged-out/private browser. +- [ ] Proof JSON link tested in logged-out/private browser. +- [ ] Final portal fields completed. +- [ ] Final Submit action completed. +- [ ] Submission receipt / confirmation retained. + +**Stop condition:** if the logged-in portal shows a mandatory gate that cannot be truthfully satisfied, do not fake it. Preserve the technical package and submit only if the official portal allows an eligible entry. + +## Crypto World's Fair — Colosseum + +Official cutoff: **12 Oct 2026, 11:59 PM PT = 13 Oct 2026, 12:59 Bangladesh time**. + +### Eligibility / registration +- [x] Official rules PDF rechecked on 7 Oct. +- [x] No-purchase requirement confirmed. +- [x] Age rule captured. +- [x] Bangladesh not listed among current excluded jurisdictions. +- [x] English-content requirement captured. +- [x] Pre-existing-code disclosure rule captured from current FAQ. +- [ ] Colosseum account/login confirmed. +- [ ] Crypto World's Fair joined. +- [ ] Team status confirmed. +- [ ] Project record created in portal. + +### Submission package +- [x] Product positioning drafted. +- [x] Technical proof ready. +- [x] Honest pre-existing-work disclosure draft ready. +- [x] Founder/market story draft ready. +- [x] Business model/GTM draft ready. +- [x] Presentation script ready. +- [x] Product demo script ready. +- [ ] Real demand/user validation added if available. +- [ ] 2–3 minute presentation video recorded. +- [ ] <=3 minute demo video recorded. +- [ ] Logo/graphic selected. +- [ ] Repository accessible to judges or review access granted. +- [ ] All content checked to be English. +- [ ] Final links tested logged-out where applicable. +- [ ] Final portal review completed. +- [ ] Final Submit action completed. +- [ ] Submission receipt retained. + +## Release rule + +Do not claim either competition is submitted until the portal itself returns a confirmation/receipt. diff --git a/docs/HACKATHON_WORK_DISCLOSURE.md b/docs/HACKATHON_WORK_DISCLOSURE.md new file mode 100644 index 0000000..ae0e77a --- /dev/null +++ b/docs/HACKATHON_WORK_DISCLOSURE.md @@ -0,0 +1,11 @@ +# Hackathon Work Disclosure + +HackCanton Season 3 delivery phase: **2026-09-18 through 2026-10-09**. + +CommitLedger may include repository history or scaffolding created before the delivery phase. Pre-existing work is not presented as hackathon-period work. + +The final submission should identify the exact submission commit and use Git history/compare ranges to show the work completed during the official delivery window. Judges should evaluate only the qualifying work under the official rules. + +Major hackathon-period areas include the Daml/Canton workflow, GitHub evidence binding, security hardening, runtime proof automation, judge-facing UI, competition compliance automation, and submission materials, to the extent their commit timestamps fall inside the official delivery period. + +Before final submission, this document must be reconciled against the actual Git history so every material claim is attributable to a qualifying commit range. diff --git a/docs/NEXT_SESSION_HANDOFF.md b/docs/NEXT_SESSION_HANDOFF.md new file mode 100644 index 0000000..b5385bb --- /dev/null +++ b/docs/NEXT_SESSION_HANDOFF.md @@ -0,0 +1,78 @@ +# NEXT SESSION HANDOFF — CommitLedger + +**Current date context: 7 October 2026.** + +Do not ask the owner to repeat project history. Read this file, `SUBMISSION_STATUS_20261007.md`, and `FINAL_CHECKLIST.md` first. + +## Active targets + +1. **HackCanton Season 3** +2. **Crypto World's Fair — Colosseum** + +The previous 1 October note that retired HackCanton is superseded by the owner's 7 October instruction. + +## Frozen technical proof + +Last fully executed, source-bound technical proof: + +`26bd992787401f6458f6685d2ab76aacd05eab4e` + +Canonical proof service: +https://commitledger-proof-final.onrender.com + +Proof endpoint: +https://commitledger-proof-final.onrender.com/api/proof + +Verified: +- Node 86/86; +- DPM 3.5.12; +- Daml build/tests; +- fresh Canton sandbox; +- six ledger transitions; +- SettlementReceipt; +- three negative/rejection proofs; +- exact source-commit binding. + +**MergeEarn is only the external GitHub evidence fixture. CommitLedger is the product.** + +## Deadline order + +### HackCanton +Hard deadline: **9 Oct 23:59 UTC / 10 Oct 05:59 Bangladesh time**. + +### Colosseum +Hard deadline: **12 Oct 23:59 PT / 13 Oct 12:59 Bangladesh time**. + +## Exact next actions + +### First: HackCanton portal +1. Log in to AppsFactory. +2. Confirm registration/eligibility/project/team state. +3. Inspect the exact remaining mandatory gates. +4. Confirm track. +5. Record the final <=5-minute video using `DEMO_SCRIPT.md`. +6. Ensure repo + proof links are judge-accessible. +7. Complete portal fields. +8. Submit and save the confirmation/receipt. + +Do not assume historical external gate names (Mana/activity/journal/profile) are universal rules; verify them in the logged-in portal. If a mandatory gate is unmet, do not fabricate compliance. + +### Second: Colosseum +1. Log in/register and join Crypto World's Fair. +2. Create/confirm the CommitLedger project entry. +3. Use the prepared submission copy in `SUBMISSION_DRAFT.md`. +4. Record `COLOSSEUM_PITCH_SCRIPT.md` and `COLOSSEUM_DEMO_SCRIPT.md`. +5. Add only real demand validation. +6. Disclose pre-existing work honestly. +7. Give judges repo access. +8. Submit and save the receipt. + +## Code-change rule + +Do not change executable code unless: +- a verified competition requirement requires it; or +- a real product defect is found. + +If executable code changes, rerun the entire proof and bind the evidence to the exact new commit. + +Owner constraint: free tools/services only unless explicitly changed. diff --git a/docs/PILOT_PLAN.md b/docs/PILOT_PLAN.md new file mode 100644 index 0000000..12c0a66 --- /dev/null +++ b/docs/PILOT_PLAN.md @@ -0,0 +1,32 @@ +# CommitLedger Pilot Plan + +## Pilot 1 — Single repository +Run CommitLedger with one maintainer-controlled open-source repository and a small set of issue bounties using DEMO_CREDIT/test value. + +Required integrations: +- GitHub API; +- Daml package; +- Canton JSON Ledger API; +- judge/operator web UI. + +Success evidence: +- canonical issue-to-PR binding; +- successful authorized settlement; +- explicit unauthorized and replay rejection; +- reproducible proof export. + +## Pilot 2 — Foundation or ecosystem bounty program +Add a foundation/community operator as the verifier role and run a defined bounty campaign with multiple repositories or maintainers. + +Required additions: +- production-grade identity/token handling; +- operational monitoring; +- organization-level access controls; +- policy for disputes and contributor identity. + +Success measures must be collected from the pilot, not invented in advance: completion rate, time-to-verification, operator effort, failed/flagged claims, and repeat usage. + +## Pilot 3 — Settlement adapter +After workflow validation, connect the verified Canton settlement state to an approved asset/payment rail appropriate for the operator. + +This is future work. The HackCanton MVP does not claim Canton Coin, fiat, MainNet payment, or production custody. diff --git a/docs/PITCH.md b/docs/PITCH.md new file mode 100644 index 0000000..85daf9e --- /dev/null +++ b/docs/PITCH.md @@ -0,0 +1,64 @@ +# HackCanton Season 3 — CommitLedger Pitch + +## 15-second version + +CommitLedger turns a verified GitHub contribution into an authorization-controlled Canton settlement receipt. GitHub proves the work event; Daml controls who may advance and settle the workflow; Canton preserves the auditable state and replay-safe receipt. + +## Problem + +Open-source bounty workflows often stop at "the PR merged." That proves a code event, but not an auditable multi-party settlement process with explicit authority over verification and settlement. + +## Product + +CommitLedger binds: +`Issue → Bounty → Claim → Pull Request → Verification → SettlementReceipt`. + +Three explicit roles participate: +- Maintainer; +- Contributor; +- Verifier. + +The backend independently checks canonical GitHub evidence before settlement state can advance. + +## Why Canton + +Without Canton/Daml, this product loses its core: +- explicit party authorization; +- stateful contractual transitions; +- consumed-contract replay protection; +- inspectable contract/update identifiers; +- final receipt bound to external evidence. + +GitHub remains the source of truth for repository events. Canton is the source of truth for authorized settlement workflow state. + +## Track 1 business case + +Primary ICP: +- open-source maintainers; +- foundations; +- ecosystem bounty programs; +- organizations paying external contributors. + +Likely buyer: +the organization operating the contributor/bounty program. + +Pilot: +1. one repository and maintainer; +2. one foundation/ecosystem campaign; +3. after validation, add an approved production settlement adapter. + +No paid customer, revenue, production transfer or adoption claim is made today. + +## Validation + +Use only executed evidence: +- real GitHub issue and merged PR fixture; +- full Node test result for the submission commit; +- Daml build and script tests; +- real Canton lifecycle; +- wrong-evidence, unauthorized-settlement and replay rejection; +- exported receipt/proof JSON. + +## Close + +CommitLedger is not "blockchain attached to GitHub." It separates evidence from authority: GitHub proves what happened, the verifier attests the canonical evidence, and Canton/Daml controls what each party is allowed to do next. diff --git a/docs/RULES_RECHECK_20260929.md b/docs/RULES_RECHECK_20260929.md new file mode 100644 index 0000000..9098732 --- /dev/null +++ b/docs/RULES_RECHECK_20260929.md @@ -0,0 +1,14 @@ +# Rules recheck - 29 September 2026 + +Result: **formal/current portal verification remains pending**. + +The earlier RULES_SNAPSHOT.md records a reported deadline of 9 October 2026, 23:59 UTC (10 October 2026, 05:59 Asia/Dhaka). That is a historical repository claim, not a freshly verified official deadline. It must not override the current registered-participant portal or rulebook. + +Recheck targets: +- https://appsfactory.cc/hackathons +- https://forum.canton.network/ +- Organizer opening-ceremony reference: https://www.youtube.com/watch?v=_PesNP65N7I + +In this execution, available portal retrieval exposed only a site shell and did not provide the formal rules or the user's registration state. Indexed secondary descriptions mention an October 9 submission date; older secondary descriptions contain different application dates. Neither is a substitute for the current organizer submission deadline and timezone. No assumption was promoted to VERIFIED. + +Keep the compliance gate blocked until official requirements are captured with the source URL, checked date, exact requirement and applicable track. Optional partner bounties must not be confused with universal entry requirements. No MainNet purchase, public-repository change or identity submission is authorized by an unverified third-party description. diff --git a/docs/RULES_RECHECK_20261007.md b/docs/RULES_RECHECK_20261007.md new file mode 100644 index 0000000..de6e75d --- /dev/null +++ b/docs/RULES_RECHECK_20261007.md @@ -0,0 +1,72 @@ +# Rules Recheck — 7 October 2026 + +This document records the current public rule/deadline facts used for the active CommitLedger submission sprint. Logged-in/private portal requirements still override this file. + +## HackCanton Season 3 + +Current public schedule: +- Opening: 17 Sep 2026. +- Build: 18 Sep–9 Oct 2026. +- Submission deadline: **9 Oct 2026, 23:59 UTC**. +- Bangladesh time: **10 Oct 2026, 05:59**. +- Closing: 10 Oct. +- Judging: 10–18 Oct. +- Finalists: 19 Oct. +- Grand Final: 21 Oct, 14:00 UTC. + +Public materials also describe: +- fully online; +- solo or team participation; +- five tracks; +- up to $50K cash + credits; +- Canton-native product focus. + +Public recheck sources: +- https://appsfactory.cc/hackathons +- https://forum.canton.network/ +- https://cantonnews.org/hackcanton-season-3-reaches-85-teams-as-submissions-begin +- https://cantonnews.org/events + +The public AppsFactory page currently exposes only a shell to unauthenticated retrieval, so private/formal portal fields are **not verified here**. + +A current ecosystem report notes that projects need the actual submission step in addition to uploading materials and mentions demo, code repository and video among the project materials. + +### Portal-only warning + +The repository contains historical external readiness gates named `mana1000`, `activity10Days`, `journalNonEmpty`, `projectProfileComplete`, and `track1Selected`. Treat these as historical portal assumptions until the logged-in current portal confirms whether they are mandatory. + +## Crypto World's Fair — Colosseum + +Official competition page: +https://colosseum.com/worldsfair + +Official rules PDF: +https://colosseum.com/legal/Crypto%20World%27s%20Fair%20Hackathon%20Rules.pdf + +Official hackathon FAQ: +https://colosseum.com/hackathon?year=fall2026 + +Verified from official rules: +- No purchase necessary. +- Contest starts **14 Sep 2026, 6:00 AM PT**. +- Contest ends **12 Oct 2026, 11:59 PM PT**. +- Bangladesh equivalent: **13 Oct 2026, 12:59**. +- Each member must register before the cutoff. +- Entrant must be age of majority in country/residence or at least 18, whichever is older. +- Bangladesh is not listed among the current excluded jurisdictions. +- One entrant may be a member of one team. +- A team may submit one project at a time. +- All submitted content must be in English. +- Judging criteria include functionality, potential impact, novelty, UX, open-source/composability and business plan. + +Verified from the current official FAQ: +- pre-existing code may be used; +- relevant prior development must be disclosed; +- judging focuses on work completed during the hackathon window; +- repository link is requested; +- private repository is allowed if review access is granted to `hackathon@colosseum.com`; +- presentation video is requested; +- <=3 minute product-demo video is requested; +- GTM, demand validation and distribution plans are requested. + +If the live portal or updated official rules differ at submission time, the live official source wins. diff --git a/docs/SUBMISSION_DRAFT.md b/docs/SUBMISSION_DRAFT.md new file mode 100644 index 0000000..f4b21bd --- /dev/null +++ b/docs/SUBMISSION_DRAFT.md @@ -0,0 +1,209 @@ +# CommitLedger — Submission Master Draft + +_Updated 7 October 2026._ + +Active targets: +1. HackCanton Season 3 +2. Crypto World's Fair — Colosseum + +## Shared one-line description + +**CommitLedger turns verified contribution work into an authorization-controlled settlement workflow with an auditable receipt.** + +## Shared problem + +A code host can prove that an issue exists and a pull request merged, but that evidence alone does not define who is authorized to verify a claim, who may settle it, or how duplicate settlement is prevented. + +## Shared solution + +CommitLedger binds canonical contribution evidence to a role-authorized workflow: + +`Issue -> Bounty -> Claim -> Pull Request -> Merge Verification -> Settlement -> SettlementReceipt` + +The verifier checks repository identity, issue binding, contributor policy, base branch, merged state, head SHA, merge commit SHA and evidence integrity before settlement can advance. + +## Technical differentiation + +Canton/Daml provides: +- role-separated Maintainer, Contributor and Verifier authority; +- explicit allowed contract transitions; +- inspectable contract/update identifiers; +- consumed-contract replay protection; +- auditable SettlementReceipt generation. + +GitHub remains the external work-evidence source. + +## Verified evidence + +Frozen proven technical commit: + +`26bd992787401f6458f6685d2ab76aacd05eab4e` + +Live proof: +https://commitledger-proof-final.onrender.com + +Proof JSON: +https://commitledger-proof-final.onrender.com/api/proof + +External evidence fixture: +- repo: `Saidur-droid/MergeEarn`; +- open issue: `#69`; +- merged PR: `#73`. + +**MergeEarn is only the proof fixture. CommitLedger is the submitted product.** + +## Integrity statement + +`DEMO_CREDIT` is test value only. + +Do not claim: +- fiat transfer; +- Canton Coin transfer; +- MainNet; +- production custody; +- customers/revenue/traction that do not exist; +- organizer endorsement. + +AI-assisted tools were used for research, planning, code review, testing, documentation and implementation support. The participant remains responsible for all submitted claims and artifacts. + +--- + +# HackCanton Season 3 + +## Suggested title + +**CommitLedger — Verifiable GitHub Work to Canton Settlement** + +## 15-second pitch + +CommitLedger turns verified GitHub contribution evidence into a role-authorized Canton workflow and an auditable SettlementReceipt. GitHub proves the work event; Daml controls who may verify and settle it; Canton preserves replay-safe workflow state. + +## Problem statement + +Open-source and contributor bounty workflows often stop at “the PR merged.” That proves a repository event, but not who is authorized to approve settlement, whether the evidence is bound to the correct issue, or whether the same work can be settled twice. + +## Solution + +CommitLedger adds an authorization-controlled settlement state machine: +1. maintainer creates a bounty for a canonical issue; +2. contributor claims it; +3. contributor submits exact PR metadata; +4. verifier checks canonical merged-PR evidence; +5. maintainer settles only verified work; +6. Canton creates the final SettlementReceipt. + +## Why Canton + +Without Canton/Daml, CommitLedger loses: +- explicit party authorization; +- contractual state transitions; +- consumed-contract replay protection; +- inspectable update/contract IDs; +- the final auditable receipt. + +## Track candidate + +**Real-World Asset & Business Workflows** is the primary candidate because the product implements an end-to-end business workflow with role authorization and audit state. Confirm the exact portal option before selecting it. + +## Demo evidence + +Show: +- canonical GitHub issue + merged PR; +- Maintainer / Contributor / Verifier roles; +- six ledger transitions; +- SettlementReceipt; +- wrong-issue rejection; +- unauthorized settlement rejection; +- duplicate/replay rejection; +- exact source commit and proof bundle. + +## Repository / proof + +Repository: use the final judge-accessible CommitLedger repository URL. + +Live proof: +https://commitledger-proof-final.onrender.com + +Proof JSON: +https://commitledger-proof-final.onrender.com/api/proof + +## Work-period disclosure + +CommitLedger includes pre-existing repository history/scaffolding. The qualifying HackCanton delivery phase added and hardened the Canton/Daml workflow, GitHub evidence binding, runtime proof automation, negative security proof, judge UI and competition materials. Reconcile the final wording with Git history before submission. + +--- + +# Crypto World's Fair — Colosseum + +## Project name + +**CommitLedger** + +## Public brief description + +CommitLedger is verifiable work-to-settlement infrastructure for open-source ecosystems and contributor programs. It binds canonical contribution evidence to role-authorized settlement state, blocks replay/duplicate settlement, and produces an auditable receipt. + +## What are you building, and who is it for? + +CommitLedger helps open-source maintainers, foundations, ecosystem programs and engineering organizations settle verified contributor work with explicit authorization and an audit trail. GitHub provides canonical issue/PR evidence. A verifier attests the evidence. Canton/Daml controls the allowed state transitions between maintainer, contributor and verifier and produces a replay-safe SettlementReceipt. + +## Why build this now? + +Contributor programs increasingly coordinate valuable work across public repositories, but the evidence, approval and settlement steps are fragmented across code hosts, spreadsheets, chats and payment tools. CommitLedger focuses on the control layer between “the work merged” and “the work is authorized for settlement,” making the decision inspectable, role-aware and resistant to duplicate settlement. + +## Technology + +- Daml / Canton for contractual workflow and authorization; +- Canton JSON Ledger API for ledger transitions and active-contract discovery; +- GitHub API for canonical issue and merged-PR evidence; +- Node.js backend/orchestration; +- browser judge UI; +- Render for the current read-only public proof service. + +AI-assisted tooling was used for research, planning, review, tests, documentation and implementation support. + +## Business model hypothesis + +CommitLedger would be sold to organizations operating contributor, bounty or ecosystem programs. A practical model is a SaaS/platform fee for workflow, policy and audit controls, with enterprise pricing for organization-level access, integrations and compliance features. Production asset/payment adapters would be added only after workflow validation. + +## Go-to-market + +1. Start with one open-source repository and a small maintainer-led bounty pilot. +2. Expand to a foundation/ecosystem contributor campaign. +3. Add organization controls, monitoring and policy integrations. +4. Add approved production settlement adapters after validation. + +## Demand validation + +Current proof is technical, not commercial traction. The project has a real end-to-end GitHub/Canton evidence fixture and executed runtime/security proof. Do not invent user interviews, pilots, revenue, wallet counts or customer demand. Add real validation only if actually collected before submission. + +## Pre-existing-work disclosure + +CommitLedger existed before the Crypto World's Fair contest window. The submission must disclose that history. Do not present the entire codebase as newly created during the contest. Identify competition-window work from Git history and describe only what was actually completed between the official start and end dates. + +## Presentation video + +Use `docs/COLOSSEUM_PITCH_SCRIPT.md`. + +## Product demo + +Use `docs/COLOSSEUM_DEMO_SCRIPT.md`. + +## Repository / links + +Repository: final judge-accessible CommitLedger URL. + +Live proof: +https://commitledger-proof-final.onrender.com + +Proof JSON: +https://commitledger-proof-final.onrender.com/api/proof + +## Final receipt fields + +Populate after actual portal actions: +- HackCanton submission URL/receipt: +- Colosseum project URL: +- Colosseum submission confirmation: +- Presentation video: +- Demo video: diff --git a/docs/SUBMISSION_STATUS_20261007.md b/docs/SUBMISSION_STATUS_20261007.md new file mode 100644 index 0000000..c5df840 --- /dev/null +++ b/docs/SUBMISSION_STATUS_20261007.md @@ -0,0 +1,77 @@ +# Submission Status — 7 October 2026 + +## Executive summary + +CommitLedger's **technical product is finished and proven**. The remaining work is primarily portal eligibility/registration, videos, judge access and final submission actions. + +### Planning readiness estimate + +These percentages are planning estimates, not organizer scores. + +| Area | HackCanton | Colosseum | +|---|---:|---:| +| Core product | 100% | 100% | +| Executed technical proof | 100% | 100% | +| Written submission material | 90% | 80% | +| Rules/deadline research | 85% | 95% | +| Recorded media | 0% | 0% | +| Portal registration/profile | Unknown until login | Unknown until login | +| Final submission/receipt | 0% | 0% | +| **Artifact-work estimate** | **~70%** | **~65%** | + +The uncertainty is external: a portal-only eligibility gate can override otherwise high artifact readiness. + +## What is already done + +- Canton/Daml workflow implemented. +- Role separation implemented. +- Canonical GitHub evidence verifier implemented. +- Full six-step lifecycle proven. +- SettlementReceipt proven. +- Three negative/security rejections proven. +- Node 86/86 PASS. +- Daml build/tests PASS. +- Frozen proof commit documented. +- Public proof service documented. +- HackCanton pitch ready. +- HackCanton demo script ready. +- Colosseum submission copy ready. +- Colosseum pitch script ready. +- Colosseum demo script ready. +- AI disclosure ready. +- Hackathon/pre-existing-work disclosure framework ready. +- Rules/deadlines rechecked on 7 Oct. + +## What remains for HackCanton + +1. Log into AppsFactory. +2. Confirm registration/eligibility/project state. +3. Confirm any Mana/activity/journal/profile gates. +4. Select/confirm track. +5. Record final <=5-minute video. +6. Make repository/proof links judge-accessible. +7. Fill final portal fields. +8. Submit. +9. Save receipt. + +## What remains for Colosseum + +1. Log in/register and join the competition. +2. Create/confirm project. +3. Add logo/graphic. +4. Add only real demand validation. +5. Record 2–3 minute presentation video. +6. Record <=3 minute demo. +7. Confirm repository review access. +8. Complete English portal fields. +9. Submit. +10. Save receipt. + +## Hard deadlines + +- HackCanton: **9 Oct 2026 23:59 UTC / 10 Oct 05:59 Bangladesh time**. +- Colosseum: **12 Oct 2026 23:59 PT / 13 Oct 12:59 Bangladesh time**. + +## Do not change + +Do not rewrite core product code merely for a new submission. If executable code changes, rerun the full technical proof. diff --git a/docs/TRACK1_BUSINESS_BRIEF.md b/docs/TRACK1_BUSINESS_BRIEF.md new file mode 100644 index 0000000..7953943 --- /dev/null +++ b/docs/TRACK1_BUSINESS_BRIEF.md @@ -0,0 +1,46 @@ +# Track 1 Business Brief — CommitLedger + +## Problem +Open-source maintainers and bounty programs can verify code changes on GitHub, but GitHub does not by itself enforce who is allowed to approve a bounty settlement, nor does it provide a multi-party settlement state machine. + +## ICP +Primary users: +- open-source maintainers running issue-based bounties; +- foundations and ecosystem grant programs; +- organizations paying contributors for verified repository work. + +## Use case +A maintainer creates a bounty tied to a canonical GitHub issue. A contributor claims it and submits a pull request. A verifier independently checks the canonical merged PR evidence. Only then can the maintainer settle. Canton records the authorized state transition and final receipt. + +## Who pays +The likely economic buyer is the organization operating the bounty or contributor program: a foundation, protocol, open-source company, ecosystem fund, or enterprise engineering organization. + +No customer revenue or paid pilot is claimed yet. + +## Why Canton +CommitLedger uses Canton/Daml for the part GitHub cannot provide: +- explicit multi-party authorization; +- stateful workflow transitions; +- privacy-compatible role separation; +- consumed-contract semantics that prevent replay of the same verified settlement; +- auditable ledger identifiers and receipt state. + +GitHub remains the source of truth for repository events; Canton is the source of truth for the authorized settlement workflow. + +## Track 1 mapping +Track 1 asks for an end-to-end business workflow with state changes, fulfillment, role-based UI, and audit/reporting. + +CommitLedger maps as: +1. Create — Bounty from a canonical GitHub issue. +2. Update — Claim and PR submission. +3. Fulfill — Independent merge verification. +4. Audit/report — SettlementReceipt with bound evidence. + +## Validation available today +Only evidence that actually exists is used: +- a real GitHub issue/merged-PR fixture in the repository; +- Node tests for canonical verification and failure classification; +- Daml contract tests when executed; +- real Canton runtime evidence only after captured from a successful run. + +No fabricated users, revenue, production transfers, wallet counts, or traction metrics are claimed. diff --git a/docs/UI_UX_BENCHMARKS.md b/docs/UI_UX_BENCHMARKS.md new file mode 100644 index 0000000..58cd53a --- /dev/null +++ b/docs/UI_UX_BENCHMARKS.md @@ -0,0 +1,83 @@ +# CommitLedger UI/UX benchmark set + +CommitLedger is a hybrid of four product categories: +1. developer workflow / source-of-truth tools; +2. bounty and contributor-workflow products; +3. audit/compliance and evidence systems; +4. on-chain / deployment operations dashboards. + +The design should borrow interaction principles, not copy any single product. + +## Benchmark products + +### GitHub +Use for: +- source-of-truth issue/PR identity; +- close connection between work items and commits/merges; +- compact status and reference formatting; +- drill-down from summary to evidence. + +Do not copy GitHub's repository chrome or exact issue layout. + +### Linear +Use for: +- calm density; +- strong hierarchy with little decorative noise; +- predictable status language; +- fast scanning and keyboard-friendly mental model. + +Do not copy Linear's sidebar, typography scale, or issue screen one-for-one. + +### Stripe Dashboard +Use for: +- inspectable identifiers; +- event/log detail; +- clear distinction between successful business state and diagnostic state; +- copyable technical evidence. + +Do not imitate Stripe branding or payment-specific visual metaphors. + +### Vercel +Use for: +- deployment/runtime status clarity; +- environment/run separation; +- commit-bound evidence; +- logs/errors as first-class diagnostic material. + +Do not copy Vercel's deployment page layout. + +### Algora +Use for: +- GitHub-native bounty context; +- compact bounty state; +- issue-linked contributor workflow. + +Do not copy its bounty list or payout presentation. + +### Gitcoin +Use conceptually for: +- grants/bounty ecosystem framing; +- ecosystem/operator context; +- transparent program state. + +Do not copy grant-round visual design. + +## CommitLedger synthesis + +The resulting product should feel like a **proof cockpit for developer settlements**: +- top summary rail: Source / Authorization / Settlement / Runtime; +- primary lifecycle: Issue → Bounty → Claim → PR → Verify → Settle → Receipt; +- exact IDs and hashes stay inspectable; +- runtime and competition readiness are visibly distinct; +- negative security proof is a first-class section; +- no runtime proof means BLOCKED, never decorative success; +- judge can understand the product in 10–15 seconds and verify it deeply in 60–90 seconds. + +## Visual direction + +- dark operational surface, high legibility; +- restrained accent color; +- borders and density inspired by developer tools, not finance marketing; +- large value statement, compact operational cards; +- no gradients or decoration that reduce evidence readability; +- mobile becomes a vertical proof narrative, not a squeezed desktop dashboard. diff --git a/docs/superpowers/plans/2026-09-29-competition-ready-delivery.md b/docs/superpowers/plans/2026-09-29-competition-ready-delivery.md new file mode 100644 index 0000000..c4ad567 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-competition-ready-delivery.md @@ -0,0 +1,161 @@ +# CommitLedger Competition-Ready Delivery Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver a HackCanton Season 3 Track 1-ready CommitLedger build with evidence-backed compliance automation, real Canton proof, judge-facing UX, and complete business/pitch materials while deferring repository visibility and final portal submission. + +**Architecture:** Keep the existing GitHub verifier -> Daml lifecycle -> Canton JSON Ledger API architecture. Add a fail-closed competition readiness model, evidence manifests, Track 1 business artifacts, runtime proof automation, and a judge-facing proof dashboard. External account/publication gates remain BLOCKED rather than being faked green. + +**Tech Stack:** Node.js 22, Daml/DPM 3.5.12, Canton JSON Ledger API v2, vanilla web UI, GitHub Actions, Figma, Linear. + +**Spec:** `docs/superpowers/specs/2026-09-29-hackcanton-s3-competition-ready-design.md` + +## Global Constraints + +- Target exactly one competition track: Track 1 — Real-World Assets (RWA) & Business Workflows. +- Submission deadline: 2026-10-09 23:59 UTC. +- Meaningful Canton integration must be demonstrated by a real runtime, not mocked transport. +- Final video must be no longer than 5 minutes. +- Public repository/public links, Mana/activity, journal, and project-profile gates are deferred external blockers; automation must report them as BLOCKED until verified. +- Never claim MainNet, Canton Coin/fiat settlement, production custody, users, traction, or metrics without evidence. +- DEMO_CREDIT is non-production test value. +- AI assistance must be disclosed in submission materials. +- Work before 2026-09-18 must be disclosed as pre-existing; judges evaluate hackathon-period work. + +## Review Focus + +- A missing external/account gate must never become PASS. +- A network/auth/rate-limit error must never count as a Daml security rejection. +- Evidence from different source commits/runs must never be combined into one green readiness result. +- UI must never render sample identifiers as if they were real runtime proof. +- Track 1 business claims must stay evidence-based and avoid invented validation. + +--- + +### Task 1: Competition readiness engine + +**Files:** +- Create: `src/competition-readiness.mjs` +- Create: `tests/competition-readiness.test.mjs` +- Create: `scripts/check-competition-readiness.mjs` +- Modify: `package.json` + +**Interfaces:** +- Produces: `evaluateCompetitionReadiness(input) -> { status, gates, counts }` +- Produces CLI JSON/Markdown summary from evidence + external-gate input. + +- [ ] Write failing tests for PASS/BLOCKED/FAIL semantics and mixed-run rejection. +- [ ] Run targeted test and verify RED. +- [ ] Implement the minimal readiness evaluator. +- [ ] Add CLI wrapper and package script. +- [ ] Run targeted test and full `npm test`. +- [ ] Commit. + +### Task 2: Track 1 business and submission package + +**Files:** +- Create: `docs/TRACK1_BUSINESS_BRIEF.md` +- Create: `docs/PILOT_PLAN.md` +- Create: `docs/AI_DISCLOSURE.md` +- Create: `docs/HACKATHON_WORK_DISCLOSURE.md` +- Modify: `docs/SUBMISSION_DRAFT.md` +- Modify: `docs/FINAL_CHECKLIST.md` + +**Interfaces:** +- Consumes official rules/spec. +- Produces judge-ready factual copy with no fabricated traction. + +- [ ] Draft Track 1 one-page business brief. +- [ ] Draft 2–3 step pilot plan. +- [ ] Add explicit AI and pre-existing-work disclosures. +- [ ] Align submission draft/checklist to official S3 gates. +- [ ] Repository content review for unsupported claims. +- [ ] Commit. + +### Task 3: Real Canton proof on connected development machine + +**Files:** +- Modify as required: `scripts/bootstrap-dpm.sh`, `scripts/run-local-proof.sh`, `scripts/verify-all.sh`, runtime code/tests. +- Evidence output: `evidence/*` (ignored except placeholders). + +**Interfaces:** +- Produces one coherent evidence bundle from a single source commit/run. +- Consumed by Task 1 readiness engine and judge UI. + +- [ ] Verify machine prerequisites and install pinned DPM 3.5.12. +- [ ] Run full Node suite. +- [ ] Run Daml build/test. +- [ ] Start fresh Canton sandbox and deploy DAR. +- [ ] Execute GitHub -> Canton lifecycle through SettlementReceipt. +- [ ] Capture wrong-evidence, unauthorized-settlement, duplicate-settlement rejections. +- [ ] Validate evidence bundle and source-commit consistency. +- [ ] If a code/script defect appears, reproduce with a failing test before fixing. +- [ ] Commit any fixes; never commit generated secrets/evidence. + +### Task 4: Judge proof dashboard and compliance view + +**Files:** +- Modify: `web/index.html` +- Modify: `web/app.js` +- Modify: `web/styles.css` +- Add/modify UI tests as supported by existing test approach. + +**Interfaces:** +- Consumes runtime proof and readiness summary. +- Produces 60–90 second judge flow and explicit six-criteria evidence view. + +- [ ] Add failing tests for readiness rendering/no fake PASS where feasible. +- [ ] Implement Value / ICP / Validation / GTM / MVP / Pitch evidence section. +- [ ] Add Track 1 badge/business workflow mapping. +- [ ] Add readiness PASS/BLOCKED/FAIL panel. +- [ ] Preserve full IDs/copy/export/rejection details. +- [ ] Run tests and browser smoke/mobile QA on connected machine. +- [ ] Commit. + +### Task 5: Figma judge-flow design + +**Files:** Figma design file; no repo code until design is approved against implemented constraints. + +**Interfaces:** +- Consumes implemented judge-flow information architecture. +- Produces editable desktop/mobile judge UI reference. + +- [ ] Create Figma file. +- [ ] Build tokens/components matching existing web implementation. +- [ ] Design hero, lifecycle, proof timeline, security rejection, readiness, business evidence, receipt views. +- [ ] Include desktop and mobile frames. +- [ ] Verify no visual state implies fabricated runtime success. + +### Task 6: Linear competition delivery system + +**Files:** Linear project/issues. + +**Interfaces:** +- Consumes official requirements and evidence gates. +- Produces persistent delivery tracker with Verified vs Blocked distinction. + +- [ ] Create `CommitLedger — HackCanton S3 Delivery` project. +- [ ] Create milestones for Compliance, Runtime Proof, Judge UX, Submission Package. +- [ ] Create issues for each mandatory rule/evidence gate. +- [ ] Link PR #8/spec where supported. +- [ ] Set deadline/target date before 2026-10-09. +- [ ] Keep publication/Mana/profile items BLOCKED until user completes external actions. + +### Task 7: Final evidence review + +**Files:** +- Modify: `docs/DELIVERY_STATUS.md` +- Modify: `docs/EVIDENCE_MANIFEST.md` +- Modify: `README.md` as needed. + +**Interfaces:** +- Consumes all prior task evidence. +- Produces truthful final pre-publication status. + +- [ ] Run full Node suite. +- [ ] Run Daml tests/runtime proof if environment remains available. +- [ ] Run competition readiness checker. +- [ ] Verify browser/mobile judge flow. +- [ ] Review repo for secrets and unsupported claims. +- [ ] Update status docs with exact evidence. +- [ ] Keep PR draft until all implementation gates are green; do not make repo public or submit without explicit final-phase instruction. diff --git a/docs/superpowers/specs/2026-09-29-hackcanton-s3-competition-ready-design.md b/docs/superpowers/specs/2026-09-29-hackcanton-s3-competition-ready-design.md new file mode 100644 index 0000000..fa49579 --- /dev/null +++ b/docs/superpowers/specs/2026-09-29-hackcanton-s3-competition-ready-design.md @@ -0,0 +1,244 @@ +# HackCanton Season 3 Competition-Ready Design + +**Status:** Design specification for CommitLedger delivery branch `fix/verified-delivery-20260929`. +**Rules snapshot:** HackCanton Season 3 live Rules/Timeline/Tracks/Materials checked 2026-09-29. +**Target track:** Track 1 — Real-World Assets (RWA) & Business Workflows. + +## 1. Goal + +Ship CommitLedger as a judge-runnable Canton application whose core value is provable end-to-end settlement of verified open-source work: + +`GitHub Issue -> Daml Bounty -> Claim -> Pull Request -> GitHub Merge Verification -> Canton Settlement -> Auditable Receipt`. + +The submission must satisfy the official Season 3 requirements and make the six judging criteria easy to verify: Value, ICP, Metrics/Validation, GTM, MVP, and Pitch. + +## 2. Competition requirements that are binding + +The final submission must: +- be built on Canton and make meaningful use of Daml, Canton nodes, or Canton APIs; +- submit to exactly one official track: Track 1; +- contain hackathon-period work from 2026-09-18 through 2026-10-09 that is clearly identifiable, with any pre-existing code disclosed; +- use a public repository with a README at final submission time; +- make every judging artifact publicly accessible without requesting access; +- provide a working prototype/live demo or recorded demo video of at most 5 minutes; +- provide concise pitch material covering problem, Canton use, target users/GTM, and validation/metrics; +- have a completed project profile, one selected track, at least one journal entry, and the submitting account's required 1,000 Mana / minimum 10 days platform activity; +- submit no later than 2026-10-09 23:59 UTC; +- avoid plagiarism, misrepresentation, artificial activity/Mana inflation, IP violations, and undisclosed AI assistance. + +Account-level gates (Mana, activity days, project profile, journal) are real submission blockers but are outside repository code. They must be verified before final submission. + +## 3. Product positioning for Track 1 + +CommitLedger is a business workflow for auditable contributor settlement. It maps cleanly to Track 1: +- **Create:** maintainer creates a bounty tied to a canonical GitHub issue. +- **Update status:** contributor claims and submits a pull request. +- **Fulfill:** verifier independently checks the merged pull request and evidence. +- **Audit/report:** Canton records the authorized settlement and final receipt. + +The demo asset remains `DEMO_CREDIT`. The product must not claim fiat transfer, Canton Coin transfer, MainNet usage, real users, production custody, or traction without evidence. + +## 4. Roles and trust boundaries + +### Maintainer +Creates the bounty, accepts the contributor claim, and performs settlement after verification. + +### Contributor +Claims the bounty and submits the GitHub pull request reference. + +### Verifier +Checks canonical GitHub evidence and advances only valid merged work. + +### Canton/Daml +Enforces role authorization, state transitions, and exactly-once settlement semantics. + +### GitHub +Provides the external source of truth for repository, issue, pull request, merge status, merge commit SHA, base branch, author policy, and merge timestamp. + +The browser is never trusted to self-report GitHub merge state or ledger success. + +## 5. Security and correctness requirements + +The MVP must fail closed. + +Required positive proof: +1. canonical issue is fetched; +2. bounty is created on Canton; +3. contributor claim is accepted; +4. real pull request is bound to that bounty; +5. canonical merged PR evidence is independently fetched; +6. merge commit is validated and reachable from the expected base branch; +7. verifier authorization succeeds; +8. maintainer settlement succeeds; +9. active SettlementReceipt is found and exported; +10. full contract/update identifiers are preserved. + +Required negative proof: +- cross-repository issue lookalikes are rejected; +- wrong issue evidence is rejected; +- unmerged or malformed merge evidence is rejected; +- unauthorized settlement is rejected; +- replay/duplicate settlement is rejected; +- network, authentication, timeout, rate-limit, and unknown infrastructure errors never count as Daml authorization/replay evidence. + +## 6. Runtime proof + +The final technical proof must run on a real Canton environment, not a mocked transport. + +Required evidence from one coherent run: +- Node test log; +- Daml build success; +- Daml Script test success; +- DAR/package deployment; +- distinct maintainer, contributor, verifier parties; +- six lifecycle ledger transitions; +- update IDs and contract IDs; +- final SettlementReceipt; +- exact negative-rejection responses; +- evidence JSON tied to the source commit; +- logs sufficient to distinguish ledger enforcement from infrastructure failure. + +Local sandbox proof is acceptable as engineering evidence unless the organizers explicitly require a different network. If HackCanton DevNet access is available, the final judge package should prefer it because it reduces judge setup friction. + +## 7. Judge-facing product flow + +The UI should make the value understandable in 60–90 seconds. + +### Screen hierarchy +1. **Problem/value statement** — merged work should not settle without independently verifiable evidence. +2. **Live Canton lifecycle** — one primary action runs the full proof. +3. **Role separation** — Maintainer / Contributor / Verifier responsibilities. +4. **Canonical GitHub evidence** — issue, PR, author, base branch, head SHA, merge commit SHA, merged timestamp. +5. **Ledger timeline** — human-readable state name plus full update ID and contract ID. +6. **Security failures** — wrong evidence, unauthorized settlement, and replay are visibly rejected. +7. **Settlement receipt** — final receipt, evidence hash, merge commit, settlement reference, and export. +8. **Why Canton** — multi-party authorization, privacy-compatible workflow semantics, and auditable state transitions. + +No fake sample transaction IDs or simulated success states may appear as if they were runtime evidence. + +## 8. Judging-criteria evidence + +### Value / Problem Statement +Explain the coordination problem: GitHub proves code events, but it does not by itself enforce who may verify or settle a bounty. + +### ICP / Audience +Primary ICP: open-source maintainers, foundations, ecosystem grant programs, and organizations running contributor bounty workflows. + +### Metrics / Validation +Use only evidence we possess: +- real GitHub issue/PR fixture; +- deterministic positive and negative tests; +- runtime Canton proof when captured; +- judge walkthrough completion. +Do not fabricate user, wallet, transaction, or revenue metrics. + +### GTM +Pilot path: +1. one repository/maintainer workflow; +2. one ecosystem or foundation bounty program; +3. integrate additional repositories and settlement adapters after validation. + +### MVP +Show real code, Daml lifecycle, GitHub verification, Canton writes/reads, role authorization, replay prevention, and evidence export. + +### Pitch +Keep the submission/demo narrative concise: +`problem -> why GitHub alone is insufficient -> live workflow -> rejected attacks -> Canton receipt -> business use -> pilot path`. + +## 9. Track 1 submission package + +Create: +- role-based working MVP; +- one-page business brief with ICP, use case, who pays, and why Canton; +- short 2–3 step pilot plan and required integrations; +- concise pitch deck/document; +- ≤5-minute recorded demo; +- public README/repository at final release; +- publicly accessible demo/project page and evidence package. + +Repository visibility changes and public publishing are intentionally deferred until the owner requests the final submission/publishing phase. + +## 10. Automation + +The delivery workflow should automate everything that can be truthfully automated: +- Node tests; +- Daml build/tests; +- Canton local proof where runner support exists; +- evidence validation; +- repository safety/compliance gates; +- artifact retention; +- browser/judge-flow smoke checks when a browser is available. + +Automation must never convert a missing real runtime into a green "submission ready" state. + +A single competition readiness check should report three categories: +- **PASS:** verified with evidence; +- **BLOCKED:** requires account, network, browser, visibility, or external state; +- **FAIL:** implementation/evidence is incorrect. + +## 11. Figma/design system + +The judge UI should be refined in Figma before final implementation polish: +- high-contrast dark interface aligned with existing code; +- one primary action per proof stage; +- clear success/rejection semantics that do not rely on color alone; +- readable full identifiers with copy affordances; +- desktop judge layout plus mobile fallback; +- no marketing-heavy decorative screens that hide evidence; +- consistent components/tokens that map directly to the web implementation. + +## 12. Delivery tracking + +Linear should track only evidence-backed work. Each competition requirement gets an issue with: +- official rule/criterion; +- implementation task; +- verification command; +- evidence artifact; +- status: Todo / In Progress / Blocked / Verified. + +Do not mark a task verified from source inspection alone when the requirement is runtime or account dependent. + +## 13. Scope exclusions until the core is verified + +Do not add: +- token/DAO/reputation systems; +- general freelance marketplace features; +- paid hosting dependencies; +- production custody; +- MainNet claims; +- fake traction; +- optional sponsor integrations that distract from the Track 1 core flow. + +Optional integrations are considered only after core compliance, runtime proof, judge UX, and submission materials are green. + +## 14. Definition of done + +CommitLedger is product-ready when: +- full Node suite is green; +- Daml build/test is green; +- a real Canton lifecycle reaches SettlementReceipt; +- the named negative cases produce specific ledger/Daml rejection evidence; +- browser judge flow passes desktop/mobile QA; +- Track 1 business brief and pilot plan are complete; +- pitch/demo material is complete and truthful; +- hackathon-period work and pre-existing work are disclosed; +- no secrets are committed; +- competition readiness checker has no implementation FAILs. + +CommitLedger is **submission-ready** only after the additional external gates pass: +- repository/public artifacts are accessible to a logged-out judge; +- 1,000 Mana requirement is satisfied; +- minimum 10 days platform activity is satisfied; +- journal is non-empty; +- project profile is complete; +- final track is Track 1; +- all links work in a private browser window; +- final submission is delivered before 2026-10-09 23:59 UTC. + +## 15. Source of truth + +Official Season 3 sources used for this spec: +- https://hackathon.appsfactory.cc/season-3 +- https://www.youtube.com/watch?v=_PesNP65N7I + +If organizers change the live rules, the newer official rule controls. Re-run the compliance check before final submission. diff --git a/evidence/.gitkeep b/evidence/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index d0a62c1..5364be4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "node --test tests/*.test.mjs", "verify:pr": "node src/verify-pr.mjs", "demo:full": "node src/run-full-demo.mjs", + "readiness": "node scripts/check-competition-readiness.mjs", "check": "npm test" }, "engines": { diff --git a/scripts/bootstrap-codespace.sh b/scripts/bootstrap-codespace.sh new file mode 100644 index 0000000..7f2468d --- /dev/null +++ b/scripts/bootstrap-codespace.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[CommitLedger] Preparing cloud development environment" +node --version +java -version +python3 --version + +bash scripts/bootstrap-dpm.sh + +if ! grep -Fq '.dpm/bin' "$HOME/.bashrc" 2>/dev/null; then + printf '\nexport PATH="$HOME/.dpm/bin:$PATH"\n' >> "$HOME/.bashrc" +fi +export PATH="$HOME/.dpm/bin:$PATH" + +echo "[CommitLedger] Running Node verification" +npm test + +echo +echo "[CommitLedger] Environment ready." +echo "For the real proof run:" +echo " export GITHUB_TOKEN=" +echo " bash scripts/run-local-proof.sh" +echo +echo "To keep the verified UI open afterward:" +echo " COMMITLEDGER_SERVE_AFTER_PROOF=true bash scripts/run-local-proof.sh" diff --git a/scripts/bootstrap-dpm.sh b/scripts/bootstrap-dpm.sh old mode 100644 new mode 100755 index 82c8bf0..83e7f55 --- a/scripts/bootstrap-dpm.sh +++ b/scripts/bootstrap-dpm.sh @@ -1,17 +1,14 @@ #!/usr/bin/env bash set -euo pipefail - -SDK_VERSION="3.5.12" - -if command -v dpm >/dev/null 2>&1; then - echo "DPM already installed: $(dpm --version || true)" - exit 0 +export PATH="${DPM_HOME:-$HOME/.dpm}/bin:$PATH" +export DPM_EDITION=open-source +if ! command -v dpm >/dev/null 2>&1; then + installer=$(mktemp) + trap 'rm -f "$installer"' EXIT + curl --fail --show-error --location --connect-timeout 15 --max-time 120 \ + https://get.digitalasset.com/install/install.sh -o "$installer" + bash "$installer" 3.5.12 +else + dpm install 3.5.12 fi - -echo "Installing open-source DPM SDK ${SDK_VERSION}..." -curl -sSL https://get.digitalasset.com/install/install.sh | DPM_EDITION=open-source bash -s "${SDK_VERSION}" - -export PATH="$HOME/.dpm/bin:$PATH" - -echo "Installed:" -dpm --version +DPM_SDK_VERSION=3.5.12 dpm version diff --git a/scripts/bootstrap-local.mjs b/scripts/bootstrap-local.mjs new file mode 100644 index 0000000..003ccc1 --- /dev/null +++ b/scripts/bootstrap-local.mjs @@ -0,0 +1,53 @@ +// This bootstrap ONLY targets an explicitly unauthenticated loopback demo ledger. +import {writeFile} from 'node:fs/promises'; +import {readFileSync} from 'node:fs'; +import {setTimeout as delay} from 'node:timers/promises'; +import {randomUUID} from 'node:crypto'; +import {CantonJsonApi} from '../src/canton-json-api.mjs'; +import {CantonApiError} from '../src/ledger-errors.mjs'; + +const baseUrl=process.env.CANTON_JSON_API_URL || 'http://127.0.0.1:3975'; +const api=new CantonJsonApi({baseUrl,insecureLocal:true}); +const suffix=randomUUID().slice(0,8); +const packageId=readFileSync('evidence/dar-package-id.txt','utf8').trim(); +const env={CANTON_JSON_API_URL:baseUrl,CANTON_INSECURE_LOCAL:'true',CANTON_PACKAGE_ID:packageId,CANTON_PACKAGE_NAME:process.env.CANTON_PACKAGE_NAME || 'commit-ledger'}; +if(!/^[a-f0-9]{64}$/.test(env.CANTON_PACKAGE_ID||'')) throw new Error('Actual DAR package ID is required'); + +async function allocateParty(role) { + const requestBody=JSON.stringify({partyIdHint:`CommitLedger-${role}-${suffix}`,identityProviderId:''}); + for(let attempt=1; attempt<=30; attempt+=1) { + try { + const result=await api.request('/v2/parties',{method:'POST',body:requestBody}); + const party=result.partyDetails?.party; + if(!party) throw new Error(`Party allocation did not return partyDetails.party for ${role}`); + return party; + } catch(error) { + const transient=error instanceof CantonApiError && error.code==='PARTY_ALLOCATION_WITHOUT_CONNECTED_SYNCHRONIZER'; + if(!transient || attempt===30) throw error; + await delay(1000); + } + } + throw new Error(`Party allocation retry loop exhausted for ${role}`); +} + +const parties=[]; +for(const role of ['MAINTAINER','CONTRIBUTOR','VERIFIER']) { + const party=await allocateParty(role); + env[`CANTON_${role}_PARTY`]=party; + parties.push(party); +} + +const userId=`commitledger-demo-${suffix}`; +await api.request('/v2/users',{method:'POST',body:JSON.stringify({ + user:{id:userId,primaryParty:parties[0],identityProviderId:'',isDeactivated:false}, + rights:parties.flatMap(party=>[ + {kind:{CanActAs:{value:{party}}}}, + {kind:{CanReadAs:{value:{party}}}} + ]) +})}); +env.CANTON_USER_ID=userId; + +const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'"; +await writeFile('.env.canton-demo.local',Object.entries(env).map(([k,v])=>`${k}=${quote(v)}`).join('\n')+'\n',{mode:0o600}); +await writeFile('evidence/ledger-setup.json',JSON.stringify({environment:'local-sandbox-no-auth',packageId:env.CANTON_PACKAGE_ID,packageName:env.CANTON_PACKAGE_NAME,parties,userId},null,2)+'\n'); +console.log('Allocated three real demo parties and one local demo user; wrote .env.canton-demo.local.'); diff --git a/scripts/check-competition-readiness.mjs b/scripts/check-competition-readiness.mjs new file mode 100644 index 0000000..3784a08 --- /dev/null +++ b/scripts/check-competition-readiness.mjs @@ -0,0 +1,15 @@ +import fs from "node:fs/promises"; +import process from "node:process"; +import { evaluateCompetitionReadiness } from "../src/competition-readiness.mjs"; + +const inputPath = process.argv[2] || process.env.COMMITLEDGER_READINESS_INPUT || "./evidence/readiness-input.json"; + +try { + const input = JSON.parse(await fs.readFile(inputPath, "utf8")); + const result = evaluateCompetitionReadiness(input); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + process.exitCode = result.status === "PASS" ? 0 : result.status === "BLOCKED" ? 2 : 1; +} catch (error) { + process.stderr.write(`Competition readiness check failed: ${error.message}\n`); + process.exitCode = 1; +} diff --git a/scripts/check-evidence.mjs b/scripts/check-evidence.mjs new file mode 100644 index 0000000..acf2006 --- /dev/null +++ b/scripts/check-evidence.mjs @@ -0,0 +1,21 @@ +import {readFile} from 'node:fs/promises'; +import {buildMergeEvidence} from '../src/github-verifier.mjs'; + +const verification=JSON.parse(await readFile('evidence/verification.json','utf8')); +if(!verification.node || !verification.damlBuild || !verification.damlTests) throw new Error('Full build/test verification is missing'); +if(!/^[a-f0-9]{40}$/.test(verification.commit||'')) throw new Error('Verification must be bound to an exact Git commit'); + +const proof=JSON.parse(await readFile('evidence/canton-proof.json','utf8')); +if(proof.schemaVersion!==2 || !/^[a-f0-9]{64}$/.test(proof.packageId||'') || !/^[A-Za-z0-9_.-]+$/.test(proof.packageName||'')) throw new Error('Real package ID, package name and current proof schema are required'); +if(proof.sourceCommit!==verification.commit) throw new Error('Node/Daml verification and Canton proof must use the same source commit'); + +const names=['BOUNTY_ON_LEDGER','CLAIM_REQUESTED','CLAIMED','PR_SUBMITTED','VERIFIED','SETTLED']; +if(proof.steps?.length!==6 || proof.steps.some((s,i)=>s.name!==names[i] || !s.updateId || !s.contractId || !Number.isSafeInteger(s.completionOffset))) throw new Error('Six complete ledger transitions are required'); +if(new Set(proof.steps.map(s=>s.contractId)).size!==6) throw new Error('Contract IDs must be distinct'); +if(proof.steps.some(s=>String(s.templateId||'').split(':').length<3)) throw new Error('Every proof step must retain its ledger template identifier'); +if(proof.negativeChecks?.length!==3 || proof.negativeChecks.some(c=>!c.rejected || !c.code || !c.response)) throw new Error('Three structured rejection responses are required'); +if(proof.settlementReceipt?.rewardUnit!=='DEMO_CREDIT') throw new Error('Expected clearly labelled demo credit receipt'); +const hash=buildMergeEvidence(proof.mergeEvidence).evidenceHash; +if(hash!==proof.mergeEvidence.evidenceHash || hash!==proof.settlementReceipt.evidenceHash) throw new Error('Receipt evidence hash mismatch'); +if(proof.settlementReceipt.mergeCommitSha!==proof.mergeEvidence.mergeCommitSha) throw new Error('Receipt merge commit mismatch'); +console.log('Technical evidence structure passes and is bound to one source commit. This does not independently authenticate the ledger or clear organizer/video/submission gates.'); diff --git a/scripts/dar-package-id.py b/scripts/dar-package-id.py new file mode 100644 index 0000000..2543aac --- /dev/null +++ b/scripts/dar-package-id.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Extract the main package ID from a built DAR using the Daml compiler's canonical inspect-dar output.""" +import json +import re +import subprocess +import sys +from pathlib import Path + +if len(sys.argv) != 2: + raise SystemExit("usage: dar-package-id.py ") + +result = subprocess.run( + ["dpm", "damlc", "inspect-dar", "--json", sys.argv[1]], + check=False, + capture_output=True, + text=True, +) +if result.returncode != 0: + sys.stderr.write(result.stderr or result.stdout) + raise SystemExit(result.returncode or 1) + +try: + payload = json.loads(result.stdout) +except json.JSONDecodeError as exc: + raise SystemExit(f"inspect-dar returned invalid JSON: {exc}") from exc + +package_id = payload.get("main_package_id") +if not isinstance(package_id, str) or not re.fullmatch(r"[a-f0-9]{64}", package_id): + raise SystemExit(f"inspect-dar returned invalid main_package_id: {package_id!r}") + +Path('evidence').mkdir(exist_ok=True) +Path('evidence/dar-package-id.txt').write_text(package_id + '\n', encoding='utf-8') +print(package_id) diff --git a/scripts/prove-and-report.sh b/scripts/prove-and-report.sh new file mode 100644 index 0000000..391155a --- /dev/null +++ b/scripts/prove-and-report.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set +e +set -uo pipefail +cd "$(dirname "$0")/.." + +echo "=== COMMITLEDGER FULL PROOF ===" +echo "Commit: $(git rev-parse HEAD)" +echo + +bash scripts/run-local-proof.sh +status=$? + +echo +echo "=== EXIT STATUS: $status ===" + +if [ "$status" -eq 0 ]; then + echo "=== SUCCESS: VERIFIED EVIDENCE ===" + test -f evidence/verification.json && cat evidence/verification.json + echo + test -f evidence/canton-proof.json && cat evidence/canton-proof.json + echo + echo "Evidence directory:" + ls -lah evidence +else + echo "=== FAILURE DIAGNOSTICS ===" + for file in evidence/verifier-trace.log evidence/canton-demo.log evidence/canton-sandbox.log evidence/daml-tests.log evidence/daml-build.log evidence/dpm-version.log evidence/node-tests.log + do + if [ -f "$file" ]; then + echo + echo "--- $file ---" + tail -n 120 "$file" + fi + done +fi + +exit "$status" diff --git a/scripts/record-demo.py b/scripts/record-demo.py new file mode 100755 index 0000000..ff0d722 --- /dev/null +++ b/scripts/record-demo.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Record only a successful real local UI run; no simulated responses or narration.""" +import json +from pathlib import Path +import shutil +import tempfile +from playwright.sync_api import sync_playwright + + +def main() -> None: + evidence = Path('evidence') + evidence.mkdir(exist_ok=True) + target = evidence / 'demo-runtime.webm' + if target.exists(): + raise RuntimeError('Existing recording retained; rename it before capturing a new run') + temp = Path(tempfile.mkdtemp(prefix='commitledger-video-')) + context = browser = None + try: + with sync_playwright() as p: + browser = p.chromium.launch() + context = browser.new_context(viewport={'width': 1440, 'height': 1000}, + record_video_dir=str(temp), + record_video_size={'width': 1440, 'height': 1000}) + page = context.new_page() + page.goto('http://127.0.0.1:4173', wait_until='networkidle') + page.locator('#live-run').wait_for() + if not page.locator('#live-run').is_enabled(): + raise RuntimeError('Real Canton lifecycle is not ready; no demo video retained') + page.wait_for_timeout(3000) + with page.expect_response(lambda r: r.url.endswith('/api/demo/run'), timeout=240000) as response: + page.locator('#live-run').click() + result = response.value + data = result.json() + proof = data.get('proof', {}) + if not result.ok or len(proof.get('steps', [])) != 6 or not proof.get('settlementReceipt'): + raise RuntimeError('Actual lifecycle did not produce six steps and a receipt') + checks = proof.get('negativeChecks', []) + if len(checks) != 3 or not all(c.get('rejected') and c.get('code') for c in checks): + raise RuntimeError('Actual negative-rejection evidence is missing') + page.locator('#receipt-card').wait_for(state='visible') + page.wait_for_timeout(4000) + for detail in page.locator('#timeline details, #receipt-card details').all(): + detail.locator('summary').click() + detail.scroll_into_view_if_needed() + page.wait_for_timeout(2500) + page.locator('#receipt-card').scroll_into_view_if_needed() + page.wait_for_timeout(5000) + video = page.video + context.close() + context = None + video.save_as(str(target)) + browser.close() + browser = None + (evidence / 'video-proof.json').write_text(json.dumps(proof, indent=2) + '\n') + print(f'Recorded a real local UI run: {target}; review before submission.') + finally: + if context: + try: + context.close() + except Exception: + pass + if browser: + try: + browser.close() + except Exception: + pass + shutil.rmtree(temp, ignore_errors=True) + + +if __name__ == '__main__': + main() diff --git a/scripts/render-proof-build.sh b/scripts/render-proof-build.sh new file mode 100644 index 0000000..cc917eb --- /dev/null +++ b/scripts/render-proof-build.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +bash scripts/bootstrap-dpm.sh +export PATH="${DPM_HOME:-$HOME/.dpm}/bin:$PATH" + +if ! command -v java >/dev/null 2>&1; then + JAVA_ROOT="$HOME/.local/temurin-21" + mkdir -p "$JAVA_ROOT" + curl --fail --show-error --location --retry 3 "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jre/hotspot/normal/eclipse" -o "$JAVA_ROOT/temurin-jre.tar.gz" + tar -xzf "$JAVA_ROOT/temurin-jre.tar.gz" -C "$JAVA_ROOT" + rm -f "$JAVA_ROOT/temurin-jre.tar.gz" + JAVA_HOME=$(find "$JAVA_ROOT" -mindepth 1 -maxdepth 1 -type d | head -n 1) + export JAVA_HOME + export PATH="$JAVA_HOME/bin:$PATH" +fi + +java -version +# Provider/bootstrap tooling may touch tracked metadata. Restore the exact checked-out commit before evidence generation. +git reset --hard HEAD +bash scripts/prove-and-report.sh diff --git a/scripts/run-local-proof.sh b/scripts/run-local-proof.sh new file mode 100755 index 0000000..84b644f --- /dev/null +++ b/scripts/run-local-proof.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -eEuo pipefail +trap 'status=$?; echo "PROOF ERROR: line $LINENO: $BASH_COMMAND (status $status)" >&2' ERR +cd "$(dirname "$0")/.." + +export PATH="${DPM_HOME:-$HOME/.dpm}/bin:$PATH" +export DPM_SDK_VERSION=3.5.12 +mkdir -p evidence +rm -f evidence/canton-proof.json evidence/ledger-setup.json evidence/canton-ports.json evidence/ledger-end.json + +SOURCE_COMMIT=$(git rev-parse HEAD) +export GITHUB_SHA="$SOURCE_COMMIT" + +CURRENT_BRANCH=$(git branch --show-current) +if [ -n "$CURRENT_BRANCH" ]; then + git fetch --quiet origin "$CURRENT_BRANCH" || true + REMOTE_COMMIT=$(git rev-parse "origin/$CURRENT_BRANCH" 2>/dev/null || true) + if [ -n "$REMOTE_COMMIT" ] && [ "$REMOTE_COMMIT" != "$SOURCE_COMMIT" ]; then + echo "BLOCKED: stale checkout. Local $SOURCE_COMMIT, origin/$CURRENT_BRANCH $REMOTE_COMMIT." >&2 + echo "Run: git reset --hard origin/$CURRENT_BRANCH" >&2 + exit 3 + fi +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo 'Tracked working-tree changes detected. Commit or stash them before generating competition evidence.' >&2 + exit 2 +fi + +if [ -z "${GITHUB_TOKEN:-}" ] && command -v gh >/dev/null 2>&1; then + GITHUB_TOKEN=$(gh auth token 2>/dev/null || true) + export GITHUB_TOKEN +fi + +node scripts/verify-all.mjs + +DAR="$PWD/daml/.daml/dist/commit-ledger-0.1.0.dar" +test -f "$DAR" + +python3 scripts/dar-package-id.py "$DAR" >/dev/null +export CANTON_PACKAGE_NAME +CANTON_PACKAGE_NAME=$(sed -n 's/^name:[[:space:]]*//p' daml/daml.yaml | head -n 1 | tr -d '\r') +if [ -z "$CANTON_PACKAGE_NAME" ]; then + echo 'Could not determine Daml package name from daml/daml.yaml.' >&2 + exit 1 +fi + +CANTON_JSON_API_PORT=$(python3 - <<'PY' +import socket +with socket.socket() as s: + s.bind(("127.0.0.1",0)) + print(s.getsockname()[1]) +PY +) +export CANTON_JSON_API_PORT +export CANTON_JSON_API_URL="http://127.0.0.1:$CANTON_JSON_API_PORT" + +echo "Using fresh Canton JSON API port $CANTON_JSON_API_PORT" + +dpm sandbox --json-api-port "$CANTON_JSON_API_PORT" --canton-port-file evidence/canton-ports.json --dar "$DAR" > evidence/canton-sandbox.log 2>&1 & +sandbox_pid=$! + +cleanup() { + if kill -0 "$sandbox_pid" 2>/dev/null; then + kill "$sandbox_pid" 2>/dev/null || true + fi + wait "$sandbox_pid" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +ready=false +for attempt in $(seq 1 180); do + if ! kill -0 "$sandbox_pid" 2>/dev/null; then + echo 'Canton sandbox exited before becoming ready.' >&2 + tail -n 100 evidence/canton-sandbox.log >&2 || true + exit 1 + fi + if test -s evidence/canton-ports.json && curl --fail --silent --max-time 2 "$CANTON_JSON_API_URL/v2/state/ledger-end" > evidence/ledger-end.json; then + ready=true + break + fi + sleep 1 +done + +if [ "$ready" != true ]; then + echo 'Canton readiness check failed after 180 seconds.' >&2 + tail -n 100 evidence/canton-sandbox.log >&2 || true + exit 1 +fi + +node scripts/bootstrap-local.mjs + +set -a +source .env.canton-demo.local +set +a +export COMMITLEDGER_EVIDENCE_FILE="$PWD/evidence/canton-proof.json" + +echo "=== STAGE 5/6: Real Canton lifecycle ===" +set +e +npm run demo:full 2>&1 | tee evidence/canton-demo.log +status=${PIPESTATUS[0]} +set -e +if [ "$status" -ne 0 ]; then + echo "=== STAGE 5/6: Real Canton lifecycle: FAILED with status $status ===" >&2 + exit "$status" +fi +echo "=== STAGE 5/6: Real Canton lifecycle: PASS ===" + +echo "=== STAGE 6/6: Evidence validation ===" +set +e +node scripts/check-evidence.mjs +status=$? +set -e +if [ "$status" -ne 0 ]; then + echo "=== STAGE 6/6: Evidence validation: FAILED with status $status ===" >&2 + exit "$status" +fi +echo "=== STAGE 6/6: Evidence validation: PASS ===" + +if [ "${COMMITLEDGER_SERVE_AFTER_PROOF:-false}" = true ]; then + npm start +fi diff --git a/scripts/verify-all.mjs b/scripts/verify-all.mjs new file mode 100644 index 0000000..410e9d9 --- /dev/null +++ b/scripts/verify-all.mjs @@ -0,0 +1,117 @@ +import { spawn } from "node:child_process"; +import { mkdirSync, rmSync, writeFileSync, appendFileSync, readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +process.chdir(root); +mkdirSync("evidence", { recursive: true }); +rmSync("evidence/verification.json", { force: true }); +rmSync("evidence/verifier-trace.log", { force: true }); + +const env = { + ...process.env, + PATH: `${process.env.DPM_HOME || `${process.env.HOME}/.dpm`}/bin:${process.env.PATH || ""}`, + DPM_SDK_VERSION: "3.5.12" +}; + +function trace(message) { + const line = `[${new Date().toISOString()}] ${message}\n`; + appendFileSync("evidence/verifier-trace.log", line); + process.stderr.write(line); +} + +async function run(label, command, args, { cwd = root, logFile } = {}) { + process.stdout.write(`=== ${label} ===\n`); + trace(`START ${label}: ${command} ${args.join(" ")} cwd=${cwd}`); + + return await new Promise((resolveRun, rejectRun) => { + const child = spawn(command, args, { + cwd, + env, + stdio: ["ignore", "pipe", "pipe"] + }); + + let output = ""; + child.stdout.on("data", chunk => { + const text = chunk.toString(); + output += text; + process.stdout.write(text); + }); + child.stderr.on("data", chunk => { + const text = chunk.toString(); + output += text; + process.stderr.write(text); + }); + child.on("error", error => { + if (logFile) writeFileSync(logFile, output); + trace(`ERROR ${label}: ${error.message}`); + rejectRun(error); + }); + child.on("close", (code, signal) => { + if (logFile) writeFileSync(logFile, output); + trace(`CLOSE ${label}: code=${code} signal=${signal || "none"}`); + if (code !== 0) { + rejectRun(new Error(`${label} failed with status ${code}${signal ? ` signal ${signal}` : ""}`)); + return; + } + process.stdout.write(`=== ${label}: PASS ===\n`); + resolveRun(); + }); + }); +} + +async function main() { + const source = await new Promise((resolveGit, rejectGit) => { + const child = spawn("git", ["rev-parse", "HEAD"], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] }); + let out = "", err = ""; + child.stdout.on("data", c => out += c.toString()); + child.stderr.on("data", c => err += c.toString()); + child.on("error", rejectGit); + child.on("close", code => code === 0 ? resolveGit(out.trim()) : rejectGit(new Error(err || "Unable to resolve Git HEAD"))); + }); + + trace(`SOURCE ${source}`); + + const testFiles = readdirSync(resolve(root, "tests")) + .filter(name => name.endsWith(".test.mjs")) + .sort() + .map(name => resolve(root, "tests", name)); + trace(`NODE_TEST_FILES ${testFiles.length}`); + await run("STAGE 1/4: Node", process.execPath, ["--test", ...testFiles], { logFile: "evidence/node-tests.log" }); + + await run("STAGE 2/4: DPM", "dpm", ["version", "--active"], { + cwd: resolve(root, "daml"), + logFile: "evidence/dpm-version.log" + }); + + await run("STAGE 3/4: Daml build", "dpm", ["build"], { + cwd: resolve(root, "daml"), + logFile: "evidence/daml-build.log" + }); + + await run("STAGE 4/4: Daml tests", "dpm", ["test"], { + cwd: resolve(root, "daml"), + logFile: "evidence/daml-tests.log" + }); + + writeFileSync( + "evidence/verification.json", + JSON.stringify({ + node: true, + damlBuild: true, + damlTests: true, + verifiedAt: new Date().toISOString(), + commit: source + }, null, 2) + "\n" + ); + + trace("COMPLETE verification"); + process.stdout.write("=== BUILD/TEST VERIFICATION COMPLETE ===\n"); +} + +main().catch(error => { + trace(`FATAL ${error?.stack || error?.message || String(error)}`); + process.stderr.write(`VERIFICATION FAILED: ${error?.message || String(error)}\n`); + process.exitCode = 1; +}); diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh old mode 100644 new mode 100755 index af8d2aa..276ced3 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -1,31 +1,3 @@ #!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" - -echo "== Node verifier tests ==" -node --version -npm test - -echo -echo "== Daml build ==" -export PATH="$HOME/.dpm/bin:$PATH" -command -v dpm >/dev/null 2>&1 || { - echo "DPM not found. Run: bash scripts/bootstrap-dpm.sh" - exit 2 -} -( - cd daml - DPM_SDK_VERSION=3.5.12 dpm build --all -) - -echo -echo "== Daml tests ==" -( - cd daml - DPM_SDK_VERSION=3.5.12 dpm test -) - -echo -echo "All local verification gates passed." +cd "$(dirname "$0")/.." || exit 1 +exec node scripts/verify-all.mjs diff --git a/src/canton-commands.mjs b/src/canton-commands.mjs index c8f9b8c..596da2a 100644 --- a/src/canton-commands.mjs +++ b/src/canton-commands.mjs @@ -4,8 +4,39 @@ function requireText(value, name) { return text; } -export function templateId(packageId, template) { - return `${requireText(packageId, "packageId")}:CommitLedger:${template}`; +function damlInt(value, name) { + const number = Number(value); + if (!Number.isSafeInteger(number)) throw new Error(`${name} must be a safe integer`); + return String(number); +} + +function normalizePullRequestRef(pullRequest) { + return { + repository: requireText(pullRequest?.repository, "pullRequest.repository"), + prNumber: damlInt(pullRequest?.prNumber, "pullRequest.prNumber"), + prUrl: requireText(pullRequest?.prUrl, "pullRequest.prUrl"), + headSha: requireText(pullRequest?.headSha, "pullRequest.headSha"), + baseBranch: requireText(pullRequest?.baseBranch, "pullRequest.baseBranch") + }; +} + +function normalizeMergeEvidence(evidence) { + return { + repository: requireText(evidence?.repository, "evidence.repository"), + issueNumber: damlInt(evidence?.issueNumber, "evidence.issueNumber"), + prNumber: damlInt(evidence?.prNumber, "evidence.prNumber"), + prUrl: requireText(evidence?.prUrl, "evidence.prUrl"), + headSha: requireText(evidence?.headSha, "evidence.headSha"), + mergeCommitSha: requireText(evidence?.mergeCommitSha, "evidence.mergeCommitSha"), + baseBranch: requireText(evidence?.baseBranch, "evidence.baseBranch"), + merged: Boolean(evidence?.merged), + mergedAt: requireText(evidence?.mergedAt, "evidence.mergedAt"), + evidenceHash: requireText(evidence?.evidenceHash, "evidence.evidenceHash") + }; +} + +export function templateId(packageName, template) { + return `#${requireText(packageName, "packageName")}:CommitLedger:${template}`; } export function createCommand(template, args) { @@ -23,13 +54,13 @@ export function exerciseCommand(template, contractId, choice, choiceArgument = { }; } -export function buildCreateBountyCommand({ packageId, maintainer, verifier, bounty }) { - return createCommand(templateId(packageId, "Bounty"), { +export function buildCreateBountyCommand({ packageName, maintainer, verifier, bounty }) { + return createCommand(templateId(packageName, "Bounty"), { maintainer: requireText(maintainer, "maintainer"), verifier: requireText(verifier, "verifier"), bountyId: requireText(bounty.bountyId, "bountyId"), repository: requireText(bounty.repository, "repository"), - issueNumber: Number(bounty.issueNumber), + issueNumber: damlInt(bounty.issueNumber, "issueNumber"), issueUrl: requireText(bounty.issueUrl, "issueUrl"), title: requireText(bounty.title, "title"), rewardAmount: requireText(bounty.rewardAmount, "rewardAmount"), @@ -37,8 +68,8 @@ export function buildCreateBountyCommand({ packageId, maintainer, verifier, boun }); } -export function buildClaimRequestCommand({ packageId, contributor, maintainer, bountyId, contributorGithub }) { - return createCommand(templateId(packageId, "ClaimRequest"), { +export function buildClaimRequestCommand({ packageName, contributor, maintainer, bountyId, contributorGithub }) { + return createCommand(templateId(packageName, "ClaimRequest"), { contributor: requireText(contributor, "contributor"), maintainer: requireText(maintainer, "maintainer"), bountyId: requireText(bountyId, "bountyId"), @@ -46,45 +77,45 @@ export function buildClaimRequestCommand({ packageId, contributor, maintainer, b }); } -export function buildAcceptClaimCommand({ packageId, claimRequestCid, bountyCid }) { +export function buildAcceptClaimCommand({ packageName, claimRequestCid, bountyCid }) { return exerciseCommand( - templateId(packageId, "ClaimRequest"), + templateId(packageName, "ClaimRequest"), claimRequestCid, "ClaimRequest_Accept", { bountyCid: requireText(bountyCid, "bountyCid") } ); } -export function buildSubmitPullRequestCommand({ packageId, claimedBountyCid, pullRequest }) { +export function buildSubmitPullRequestCommand({ packageName, claimedBountyCid, pullRequest }) { return exerciseCommand( - templateId(packageId, "ClaimedBounty"), + templateId(packageName, "ClaimedBounty"), claimedBountyCid, "ClaimedBounty_SubmitPullRequest", - { pullRequest } + { pullRequest: normalizePullRequestRef(pullRequest) } ); } -export function buildReturnForRevisionCommand({ packageId, submittedBountyCid, reason }) { +export function buildReturnForRevisionCommand({ packageName, submittedBountyCid, reason }) { return exerciseCommand( - templateId(packageId, "SubmittedBounty"), + templateId(packageName, "SubmittedBounty"), submittedBountyCid, "SubmittedBounty_ReturnForRevision", { reason: requireText(reason, "reason") } ); } -export function buildVerifyMergedCommand({ packageId, submittedBountyCid, evidence }) { +export function buildVerifyMergedCommand({ packageName, submittedBountyCid, evidence }) { return exerciseCommand( - templateId(packageId, "SubmittedBounty"), + templateId(packageName, "SubmittedBounty"), submittedBountyCid, "SubmittedBounty_VerifyMerged", - { evidence } + { evidence: normalizeMergeEvidence(evidence) } ); } -export function buildSettleCommand({ packageId, verifiedBountyCid, settledAt, settlementRef }) { +export function buildSettleCommand({ packageName, verifiedBountyCid, settledAt, settlementRef }) { return exerciseCommand( - templateId(packageId, "VerifiedBounty"), + templateId(packageName, "VerifiedBounty"), verifiedBountyCid, "VerifiedBounty_Settle", { diff --git a/src/canton-json-api.mjs b/src/canton-json-api.mjs index f3e8e2a..47b4a3a 100644 --- a/src/canton-json-api.mjs +++ b/src/canton-json-api.mjs @@ -1,12 +1,46 @@ +import { CantonApiError } from "./ledger-errors.mjs"; + function requireText(value, name) { const text = String(value || "").trim(); if (!text) throw new Error(`${name} is required`); return text; } +function encodeDamlValue(value) { + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new Error("Daml numeric values must be finite"); + return String(value); + } + if (Array.isArray(value)) return value.map(encodeDamlValue); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, encodeDamlValue(nested)])); + } + return value; +} + +function encodeCommandDamlValues(command) { + if (command?.CreateCommand) { + return { + CreateCommand: { + ...command.CreateCommand, + createArguments: encodeDamlValue(command.CreateCommand.createArguments) + } + }; + } + if (command?.ExerciseCommand) { + return { + ExerciseCommand: { + ...command.ExerciseCommand, + choiceArgument: encodeDamlValue(command.ExerciseCommand.choiceArgument) + } + }; + } + return command; +} + function authHeaders(token) { return { - Authorization: `Bearer ${requireText(token, "Canton token")}`, + ...(token ? { Authorization: `Bearer ${token}` } : {}), "Content-Type": "application/json" }; } @@ -40,16 +74,28 @@ export function extractCreatedEvents(activeContractsResponse) { .filter(Boolean); } +export function extractTransactionCreatedEvents(response) { + const transaction = response?.transaction?.value || response?.transaction; + const events = transaction?.events; + if (!Array.isArray(events)) return []; + return events + .map(event => event?.CreatedEvent?.value || event?.CreatedEvent) + .filter(Boolean); +} + export class CantonJsonApi { - constructor({ baseUrl, token, applicationId = "commit-ledger" }) { + constructor({ baseUrl, token, userId = "", insecureLocal = false }) { this.baseUrl = requireText(baseUrl, "Canton base URL").replace(/\/$/, ""); - this.token = requireText(token, "Canton token"); - this.applicationId = applicationId; + const url = new URL(this.baseUrl); + if (insecureLocal && !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) throw new Error("insecure Canton mode requires loopback"); + this.token = insecureLocal ? String(token || "") : requireText(token, "Canton token"); + this.userId = userId; } async request(path, options = {}) { const response = await fetch(`${this.baseUrl}${path}`, { ...options, + signal: options.signal || AbortSignal.timeout(30_000), headers: { ...authHeaders(this.token), ...(options.headers || {}) @@ -60,25 +106,46 @@ export class CantonJsonApi { if (text) { try { body = JSON.parse(text); } catch { body = { raw: text }; } } - if (!response.ok) throw new Error(`Canton JSON Ledger API failed: ${response.status} ${text}`); + if (!response.ok) throw new CantonApiError(response.status, body); return body; } - async submitAndWait({ commands, actAs, readAs = [], workflowId, commandId }) { + async submitAndWait({ commands, actAs, readAs = [], workflowId, commandId, packageIdSelectionPreference = this.packageIdSelectionPreference || [] }) { return this.request("/v2/commands/submit-and-wait", { method: "POST", body: JSON.stringify({ - commands, + commands: commands.map(encodeCommandDamlValues), workflowId, - applicationId: this.applicationId, + userId: this.userId, commandId, deduplicationPeriod: { Empty: {} }, actAs, readAs, submissionId: commandId, disclosedContracts: [], - domainId: "", - packageIdSelectionPreference: [] + synchronizerId: "", + packageIdSelectionPreference + }) + }); + } + + async submitAndWaitForTransaction({ commands, actAs, readAs = [], workflowId, commandId, packageIdSelectionPreference = this.packageIdSelectionPreference || [] }) { + return this.request("/v2/commands/submit-and-wait-for-transaction", { + method: "POST", + body: JSON.stringify({ + commands: { + commands: commands.map(encodeCommandDamlValues), + workflowId, + userId: this.userId, + commandId, + deduplicationPeriod: { Empty: {} }, + actAs, + readAs, + submissionId: commandId, + disclosedContracts: [], + synchronizerId: "", + packageIdSelectionPreference + } }) }); } @@ -87,7 +154,7 @@ export class CantonJsonApi { return this.request("/v2/state/active-contracts", { method: "POST", body: JSON.stringify({ - activeAtOffset, + activeAtOffset: Number(activeAtOffset), eventFormat: eventFormatForParty(party, templateId) }) }); @@ -96,8 +163,14 @@ export class CantonJsonApi { async findActiveContract({ party, templateId, activeAtOffset, predicate = () => true }) { const raw = await this.activeContracts({ party, templateId, activeAtOffset }); const events = extractCreatedEvents(raw); - const event = events.find(candidate => predicate(candidate?.createArgument || {})); - if (!event) throw new Error(`Active contract not found for template ${templateId} at offset ${activeAtOffset}`); + const requestedEntity = String(templateId).split(':').slice(-2).join(':'); + const matches = events.filter(candidate => { + const candidateEntity = String(candidate?.templateId || '').split(':').slice(-2).join(':'); + return candidateEntity === requestedEntity && predicate(candidate?.createArgument || {}); + }); + if (matches.length > 1) throw new Error(`Ambiguous active contracts for template ${templateId}`); + const event = matches[0]; + if (!event?.contractId) throw new Error(`Active contract not found for template ${templateId} at offset ${activeAtOffset}`); return event; } } diff --git a/src/competition-readiness.mjs b/src/competition-readiness.mjs new file mode 100644 index 0000000..43d4823 --- /dev/null +++ b/src/competition-readiness.mjs @@ -0,0 +1,85 @@ +const IMPLEMENTATION_GATES = [ + "nodeTests", + "damlBuild", + "damlTests", + "cantonLifecycle", + "negativeSecurity", + "judgeUi", + "track1Package", +]; + +const EXTERNAL_GATES = [ + "publicRepository", + "publicArtifacts", + "mana1000", + "activity10Days", + "journalNonEmpty", + "projectProfileComplete", + "track1Selected", +]; + +function normalizeStatus(value, kind) { + const raw = String(value?.status || "").toLowerCase(); + if (raw === "verified" || raw === "pass") return "PASS"; + if (raw === "failed" || raw === "fail") return "FAIL"; + if (raw === "blocked" || raw === "pending" || raw === "unknown" || raw === "") return "BLOCKED"; + return kind === "implementation" ? "FAIL" : "BLOCKED"; +} + +function gateRecord(name, value, kind) { + return { + name, + kind, + status: normalizeStatus(value, kind), + reason: value?.reason || "", + runId: value?.runId || "", + sourceCommit: value?.sourceCommit || "", + }; +} + +function coherenceGate(implementation) { + const verifiedRuntime = IMPLEMENTATION_GATES + .map(name => [name, implementation?.[name]]) + .filter(([, value]) => normalizeStatus(value, "implementation") === "PASS") + .filter(([, value]) => value?.runId || value?.sourceCommit); + + if (verifiedRuntime.length === 0) return null; + + const runIds = new Set(verifiedRuntime.map(([, value]) => value?.runId).filter(Boolean)); + const sourceCommits = new Set(verifiedRuntime.map(([, value]) => value?.sourceCommit).filter(Boolean)); + if (runIds.size <= 1 && sourceCommits.size <= 1) return null; + + return { + name: "evidenceCoherence", + kind: "implementation", + status: "FAIL", + reason: "Verified implementation evidence must come from one coherent run and source commit.", + runId: "", + sourceCommit: "", + }; +} + +export function evaluateCompetitionReadiness({ implementation = {}, external = {} } = {}) { + const gates = [ + ...IMPLEMENTATION_GATES.map(name => gateRecord(name, implementation[name], "implementation")), + ...EXTERNAL_GATES.map(name => gateRecord(name, external[name], "external")), + ]; + + const coherence = coherenceGate(implementation); + if (coherence) gates.push(coherence); + + const counts = gates.reduce((acc, gate) => { + if (gate.status === "PASS") acc.pass += 1; + else if (gate.status === "FAIL") acc.fail += 1; + else acc.blocked += 1; + return acc; + }, { pass: 0, blocked: 0, fail: 0 }); + + const status = counts.fail > 0 ? "FAIL" : counts.blocked > 0 ? "BLOCKED" : "PASS"; + return { status, gates, counts }; +} + +export const competitionGateNames = Object.freeze({ + implementation: [...IMPLEMENTATION_GATES], + external: [...EXTERNAL_GATES], +}); diff --git a/src/github-verifier.mjs b/src/github-verifier.mjs index 6a3801e..9f6e045 100644 --- a/src/github-verifier.mjs +++ b/src/github-verifier.mjs @@ -17,11 +17,13 @@ export function parseGitHubIssueUrl(value) { } catch { throw new Error("Enter a valid GitHub issue URL"); } + if (url.protocol !== "https:" || url.username || url.password || url.port) throw new Error("Issue URL must use HTTPS without credentials or a custom port"); if (!["github.com", "www.github.com"].includes(url.hostname)) throw new Error("Issue URL must point to github.com"); const parts = url.pathname.split("/").filter(Boolean); if (parts.length !== 4 || parts[2] !== "issues") throw new Error("Use github.com/owner/repo/issues/123"); + if (!/^[1-9][0-9]*$/.test(parts[3])) throw new Error("Invalid GitHub issue number"); const issueNumber = Number(parts[3]); - if (!Number.isInteger(issueNumber) || issueNumber <= 0) throw new Error("Invalid GitHub issue number"); + if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0) throw new Error("Invalid GitHub issue number"); return { owner: parts[0], repo: parts[1], repository: `${parts[0]}/${parts[1]}`, issueNumber }; } @@ -31,8 +33,12 @@ export function normalizeIssue(payload, repository) { if (!Number.isInteger(payload.number) || !payload.html_url || !payload.title) { throw new Error("Incomplete GitHub issue payload"); } + const canonical = parseGitHubIssueUrl(payload.html_url); + if (canonical.repository.toLowerCase() !== repository.toLowerCase() || canonical.issueNumber !== payload.number) { + throw new Error("canonical GitHub issue identity mismatch"); + } return { - repository, + repository: canonical.repository, issueNumber: payload.number, issueUrl: payload.html_url, title: payload.title, @@ -46,10 +52,11 @@ export async function fetchGitHubIssue({ issueUrl, token, requireOpen = true }) const parsed = parseGitHubIssueUrl(issueUrl); const response = await fetch( `https://api.github.com/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/issues/${parsed.issueNumber}`, - { headers: githubHeaders(token) } + { headers: githubHeaders(token), signal: AbortSignal.timeout(30_000) } ); if (!response.ok) throw new Error(`GitHub issue lookup failed: ${response.status} ${response.statusText}`); const issue = normalizeIssue(await response.json(), parsed.repository); + if (issue.issueNumber !== parsed.issueNumber) throw new Error("GitHub issue number mismatch"); if (requireOpen && issue.state !== "open") throw new Error("Bounty source issue must be open"); return issue; } @@ -60,6 +67,7 @@ export function normalizePullRequest(payload) { const prNumber = payload.number; const prUrl = payload.html_url; const headSha = payload.head?.sha; + const mergeCommitSha = payload.merge_commit_sha ?? ""; const baseBranch = payload.base?.ref; const merged = payload.merged === true; const mergedAt = payload.merged_at ?? ""; @@ -70,7 +78,7 @@ export function normalizePullRequest(payload) { throw new Error("Incomplete GitHub pull request payload"); } - return { repository, prNumber, prUrl, headSha, baseBranch, merged, mergedAt, author, body }; + return { repository, prNumber, prUrl, headSha, mergeCommitSha, baseBranch, merged, mergedAt, author, body }; } export function assertExpectedPullRequest(pr, expected) { @@ -81,14 +89,22 @@ export function assertExpectedPullRequest(pr, expected) { if (expected.contributorGithub && pr.author.toLowerCase() !== expected.contributorGithub.toLowerCase()) { throw new Error("pull request author mismatch"); } - if (expected.issueNumber) { + if (!pr.merged) throw new Error("pull request is not merged"); + if (!pr.mergedAt || !Number.isFinite(Date.parse(pr.mergedAt))) throw new Error("merged pull request is missing valid merged_at"); + if (!/^[a-f0-9]{40}$/i.test(pr.headSha)) throw new Error("invalid head SHA"); + if (!/^[a-f0-9]{40}$/i.test(pr.mergeCommitSha)) throw new Error("merged pull request is missing valid merge commit SHA"); + if (expected.mergeCommitSha && pr.mergeCommitSha !== expected.mergeCommitSha) throw new Error("merge commit SHA mismatch"); + const prUrl = new URL(pr.prUrl); + if (prUrl.protocol !== "https:" || prUrl.hostname !== "github.com" || prUrl.username || prUrl.password || prUrl.port || + prUrl.pathname.toLowerCase() !== `/${pr.repository}/pull/${pr.prNumber}`.toLowerCase()) { + throw new Error("canonical pull request URL mismatch"); + } + if (expected.issueNumber !== undefined) { const issueNumber = Number(expected.issueNumber); - const issueRef = new RegExp("(?:#|issues/)" + issueNumber + "\\b", "i"); - if (!issueRef.test(pr.body)) throw new Error("pull request does not reference the bounty issue"); + if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0) throw new Error("invalid bounty issue number"); + if (!referencesIssue(pr.body, expected.repository, issueNumber)) throw new Error("pull request does not reference the bounty issue"); pr.issueNumber = issueNumber; } - if (!pr.merged) throw new Error("pull request is not merged"); - if (!pr.mergedAt) throw new Error("merged pull request is missing merged_at"); return pr; } @@ -96,12 +112,14 @@ export function buildMergeEvidence(pr) { if (!Number.isInteger(pr.issueNumber) || pr.issueNumber <= 0) { throw new Error("verified pull request is missing the bounty issue number"); } + if (!pr.merged || !/^[a-f0-9]{40}$/i.test(pr.mergeCommitSha)) throw new Error("valid merged commit evidence is required"); const canonical = JSON.stringify({ repository: pr.repository, issueNumber: pr.issueNumber, prNumber: pr.prNumber, prUrl: pr.prUrl, headSha: pr.headSha, + mergeCommitSha: pr.mergeCommitSha, baseBranch: pr.baseBranch, merged: pr.merged, mergedAt: pr.mergedAt @@ -112,6 +130,7 @@ export function buildMergeEvidence(pr) { prNumber: pr.prNumber, prUrl: pr.prUrl, headSha: pr.headSha, + mergeCommitSha: pr.mergeCommitSha, baseBranch: pr.baseBranch, merged: pr.merged, mergedAt: pr.mergedAt, @@ -119,13 +138,55 @@ export function buildMergeEvidence(pr) { }; } -export async function fetchVerifiedPullRequest({ repository, prNumber, token, expected = {} }) { - const response = await fetch(`https://api.github.com/repos/${repository}/pulls/${prNumber}`, { - headers: githubHeaders(token) +/** Explicit issue references only; mentions are not proof of semantic issue resolution. */ +export function referencesIssue(body, repository, issueNumber) { + let found = false; + const expected = repository.toLowerCase(); + function inspectUrl(raw) { + try { + const url = new URL(raw.replace(/[.,;!?]+$/, "")); + if (url.protocol === "https:" && url.hostname === "github.com" && !url.username && !url.password && !url.port && + url.pathname.replace(/\/$/, "").toLowerCase() === `/${expected}/issues/${issueNumber}`) found = true; + } catch { /* Unknown URL shapes never become local references. */ } + } + let text = String(body || "").replace(/|```[\s\S]*?```|`[^`]*`/g, " "); + // Remove entire Markdown links so a misleading label cannot override its target. + text = text.replace(/\[[^\]]*\]\(([^\s)]+)(?:\s+"[^"]*")?\)/g, (_match, url) => { + inspectUrl(url); + return " "; + }); + // Consume even foreign URLs before looking for bare #number references. + text = text.replace(/(?:[a-z][a-z0-9+.-]*:\/\/|www\.)[^\s<>"'`()]+/gi, url => { + inspectUrl(url); + return " "; + }); + text = text.replace(/(?:^|[^\w./-])([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9][0-9]*)(?![\w/-])/g, (_match, repo, number) => { + if (repo.toLowerCase() === expected && Number(number) === issueNumber) found = true; + return " "; + }); + const local = /(?:^|[\s([{,:;])#([1-9][0-9]*)(?=$|[\s)\]},.!?;:])/g; + for (const match of text.matchAll(local)) if (Number(match[1]) === issueNumber) found = true; + return found; +} + +async function githubJson(path, token) { + const response = await fetch(`https://api.github.com/repos/${path}`, { + headers: githubHeaders(token), signal: AbortSignal.timeout(30_000) }); if (!response.ok) throw new Error(`GitHub API failed: ${response.status} ${response.statusText}`); + return response.json(); +} - const pr = normalizePullRequest(await response.json()); - assertExpectedPullRequest(pr, { repository, prNumber, ...expected }); +export async function fetchVerifiedPullRequest({ repository, prNumber, token, expected = {} }) { + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || !Number.isSafeInteger(prNumber) || prNumber <= 0) { + throw new Error("invalid repository or pull request number"); + } + const pr = normalizePullRequest(await githubJson(`${repository}/pulls/${prNumber}`, token)); + // Expected values cannot override the canonical repository/PR requested above. + assertExpectedPullRequest(pr, { ...expected, repository, prNumber }); + const commit = await githubJson(`${repository}/commits/${pr.mergeCommitSha}`, token); + if (commit.sha !== pr.mergeCommitSha) throw new Error("canonical merge commit SHA mismatch"); + const comparison = await githubJson(`${repository}/compare/${pr.mergeCommitSha}...${encodeURIComponent(pr.baseBranch)}`, token); + if (!["ahead", "identical"].includes(comparison.status)) throw new Error("merge commit is not reachable from the expected base branch"); return buildMergeEvidence(pr); } diff --git a/src/ledger-errors.mjs b/src/ledger-errors.mjs new file mode 100644 index 0000000..9f7a1dc --- /dev/null +++ b/src/ledger-errors.mjs @@ -0,0 +1,30 @@ +/** Fail closed: connectivity, authentication and unknown errors are not security proof. */ +export class CantonApiError extends Error { + constructor(status, body) { + const rawCode = typeof body?.code === "string" ? body.code : "UNKNOWN_CANTON_ERROR"; + const cause = typeof body?.cause === "string" ? body.cause : "Unrecognized Canton error response"; + const code = rawCode === "DAML_FAILURE" && cause.includes("UNHANDLED_EXCEPTION/") + ? "DAML_UNHANDLED_EXCEPTION" + : rawCode; + super(`Canton JSON Ledger API failed: ${status} ${code}: ${cause}`); + this.name = "CantonApiError"; + this.status = status; + this.code = code; + this.body = body; + } +} + +export async function expectLedgerFailure(name, action, expected) { + try { + await action(); + } catch (error) { + const codeMatches = error instanceof CantonApiError && expected.codes.includes(error.code); + const statusMatches = error?.status >= 400 && error.status < 500 && ![401, 403, 408, 429].includes(error.status); + const detailMatches = !expected.contains || JSON.stringify(error?.body || {}).includes(expected.contains); + if (!codeMatches || !statusMatches || !detailMatches) { + throw new Error(`Unproven ledger rejection (${name}): ${error?.message || String(error)}`, { cause: error }); + } + return { name, rejected: true, code: error.code, httpStatus: error.status, response: error.body }; + } + throw new Error(`Expected Canton rejection did not occur: ${name}`); +} diff --git a/src/orchestrator.mjs b/src/orchestrator.mjs index da2be4b..3b0016f 100644 --- a/src/orchestrator.mjs +++ b/src/orchestrator.mjs @@ -1,4 +1,6 @@ -import { CantonJsonApi } from "./canton-json-api.mjs"; +import { randomUUID } from "node:crypto"; +import { expectLedgerFailure } from "./ledger-errors.mjs"; +import { CantonJsonApi, extractTransactionCreatedEvents } from "./canton-json-api.mjs"; import { templateId, buildCreateBountyCommand, @@ -19,7 +21,7 @@ function required(value, name) { function offsetOf(result) { const offset = Number(result?.completionOffset); - if (!result?.updateId || !Number.isFinite(offset)) { + if (!result?.updateId || !Number.isSafeInteger(offset) || offset <= 0) { throw new Error("Canton submission did not return updateId and completionOffset"); } return offset; @@ -36,17 +38,10 @@ function proofStep(name, submission, event) { }; } -function roleClient(baseUrl, token) { - return new CantonJsonApi({ baseUrl, token }); -} - -async function expectLedgerFailure(name, action) { - try { - await action(); - } catch (error) { - return { name, rejected: true, error: error.message }; - } - throw new Error(`Expected Canton rejection did not occur: ${name}`); +function roleClient(baseUrl, token, runtime) { + const client = new CantonJsonApi({ baseUrl, token, userId: runtime.userId || "", insecureLocal: runtime.insecureLocal || false }); + client.packageIdSelectionPreference = runtime.packageId ? [runtime.packageId] : []; + return client; } async function submitAndFind({ @@ -58,35 +53,55 @@ async function submitAndFind({ lookupTemplateId, predicate }) { - const submission = await client.submitAndWait({ + const response = await client.submitAndWaitForTransaction({ commands: [command], actAs: [actAs], workflowId, - commandId: `${workflowId}-${Date.now()}-${Math.random().toString(16).slice(2)}` + commandId: `${workflowId}-${Date.now()}-${Math.random().toString(16).slice(2)}`, + packageIdSelectionPreference: client.packageIdSelectionPreference || [] }); - const event = await client.findActiveContract({ - party: lookupParty, - templateId: lookupTemplateId, - activeAtOffset: offsetOf(submission), - predicate + const transaction = response?.transaction?.value || response?.transaction; + const offset = Number(transaction?.offset); + if (!transaction?.updateId || !Number.isSafeInteger(offset) || offset <= 0) { + throw new Error("Canton transaction response did not return updateId and offset"); + } + const requestedEntity = String(lookupTemplateId).split(":").slice(-2).join(":"); + const matches = extractTransactionCreatedEvents(response).filter(candidate => { + const candidateEntity = String(candidate?.templateId || "").split(":").slice(-2).join(":"); + return candidateEntity === requestedEntity && predicate(candidate?.createArgument || {}); }); - return { submission, event }; + if (matches.length !== 1) { + throw new Error(`Expected exactly one created ${requestedEntity} contract, found ${matches.length}`); + } + return { + submission: { updateId: transaction.updateId, completionOffset: offset }, + event: matches[0] + }; } export function runtimeConfigFromEnv(env = process.env) { const fallbackToken = env.CANTON_TOKEN || ""; + const insecureLocal = env.CANTON_INSECURE_LOCAL === "true"; + const baseUrl = required(env.CANTON_JSON_API_URL, "CANTON_JSON_API_URL"); + if (insecureLocal && !["localhost", "127.0.0.1", "[::1]"].includes(new URL(baseUrl).hostname)) { + throw new Error("insecure Canton mode requires loopback"); + } + const token = (value, label) => insecureLocal ? String(value || "") : required(value, label); return { - baseUrl: required(env.CANTON_JSON_API_URL, "CANTON_JSON_API_URL"), + insecureLocal, + userId: env.CANTON_USER_ID || "", + baseUrl, packageId: required(env.CANTON_PACKAGE_ID, "CANTON_PACKAGE_ID"), + packageName: required(env.CANTON_PACKAGE_NAME || "commit-ledger", "CANTON_PACKAGE_NAME"), parties: assertDistinctParties({ maintainer: env.CANTON_MAINTAINER_PARTY, contributor: env.CANTON_CONTRIBUTOR_PARTY, verifier: env.CANTON_VERIFIER_PARTY }), tokens: { - maintainer: required(env.CANTON_MAINTAINER_TOKEN || fallbackToken, "CANTON_MAINTAINER_TOKEN or CANTON_TOKEN"), - contributor: required(env.CANTON_CONTRIBUTOR_TOKEN || fallbackToken, "CANTON_CONTRIBUTOR_TOKEN or CANTON_TOKEN"), - verifier: required(env.CANTON_VERIFIER_TOKEN || fallbackToken, "CANTON_VERIFIER_TOKEN or CANTON_TOKEN") + maintainer: token(env.CANTON_MAINTAINER_TOKEN || fallbackToken, "CANTON_MAINTAINER_TOKEN or CANTON_TOKEN"), + contributor: token(env.CANTON_CONTRIBUTOR_TOKEN || fallbackToken, "CANTON_CONTRIBUTOR_TOKEN or CANTON_TOKEN"), + verifier: token(env.CANTON_VERIFIER_TOKEN || fallbackToken, "CANTON_VERIFIER_TOKEN or CANTON_TOKEN") } }; } @@ -102,8 +117,12 @@ export async function runFullLifecycle({ now = () => new Date() }) { const { baseUrl, packageId, parties, tokens } = runtime; + const packageName = runtime.packageName || "commit-ledger"; + const runId = randomUUID(); + contributorGithub = required(contributorGithub, "contributorGithub"); const issue = await fetchGitHubIssue({ issueUrl, token: githubToken }); const bounty = validateBountyDraft({ + bountyId: `demo-${runId}`, repository: issue.repository, issueNumber: issue.issueNumber, issueUrl: issue.issueUrl, @@ -112,23 +131,40 @@ export async function runFullLifecycle({ rewardUnit: "DEMO_CREDIT" }); - const maintainerApi = roleClient(baseUrl, tokens.maintainer); - const contributorApi = roleClient(baseUrl, tokens.contributor); - const verifierApi = roleClient(baseUrl, tokens.verifier); + const evidence = await fetchVerifiedPullRequest({ + repository: bounty.repository, + prNumber: Number(prNumber), + token: githubToken, + expected: { + baseBranch, + contributorGithub, + issueNumber: bounty.issueNumber + } + }); + + const maintainerApi = roleClient(baseUrl, tokens.maintainer, runtime); + const contributorApi = roleClient(baseUrl, tokens.contributor, runtime); + const verifierApi = roleClient(baseUrl, tokens.verifier, runtime); const proof = { + schemaVersion: 2, + runId, + packageId, + packageName, + generatedAt: now().toISOString(), + environment: runtime.insecureLocal ? "local-sandbox-no-auth" : "authenticated-ledger", issue, bounty, parties, steps: [], negativeChecks: [], - mergeEvidence: null, + mergeEvidence: evidence, settlementReceipt: null }; const bountyCreated = await submitAndFind({ client: maintainerApi, command: buildCreateBountyCommand({ - packageId, + packageName, maintainer: parties.maintainer, verifier: parties.verifier, bounty @@ -136,7 +172,7 @@ export async function runFullLifecycle({ actAs: parties.maintainer, workflowId: "commitledger-create-bounty", lookupParty: parties.maintainer, - lookupTemplateId: templateId(packageId, "Bounty"), + lookupTemplateId: templateId(packageName, "Bounty"), predicate: arg => arg.bountyId === bounty.bountyId }); proof.steps.push(proofStep("BOUNTY_ON_LEDGER", bountyCreated.submission, bountyCreated.event)); @@ -144,7 +180,7 @@ export async function runFullLifecycle({ const claimCreated = await submitAndFind({ client: contributorApi, command: buildClaimRequestCommand({ - packageId, + packageName, contributor: parties.contributor, maintainer: parties.maintainer, bountyId: bounty.bountyId, @@ -153,7 +189,7 @@ export async function runFullLifecycle({ actAs: parties.contributor, workflowId: "commitledger-claim-request", lookupParty: parties.contributor, - lookupTemplateId: templateId(packageId, "ClaimRequest"), + lookupTemplateId: templateId(packageName, "ClaimRequest"), predicate: arg => arg.bountyId === bounty.bountyId && arg.contributorGithub === contributorGithub }); proof.steps.push(proofStep("CLAIM_REQUESTED", claimCreated.submission, claimCreated.event)); @@ -161,30 +197,18 @@ export async function runFullLifecycle({ const claimAccepted = await submitAndFind({ client: maintainerApi, command: buildAcceptClaimCommand({ - packageId, + packageName, claimRequestCid: claimCreated.event.contractId, bountyCid: bountyCreated.event.contractId }), actAs: parties.maintainer, workflowId: "commitledger-accept-claim", lookupParty: parties.maintainer, - lookupTemplateId: templateId(packageId, "ClaimedBounty"), + lookupTemplateId: templateId(packageName, "ClaimedBounty"), predicate: arg => arg.bountyId === bounty.bountyId && arg.contributorGithub === contributorGithub }); proof.steps.push(proofStep("CLAIMED", claimAccepted.submission, claimAccepted.event)); - const evidence = await fetchVerifiedPullRequest({ - repository: bounty.repository, - prNumber: Number(prNumber), - token: githubToken, - expected: { - baseBranch, - contributorGithub, - issueNumber: bounty.issueNumber - } - }); - proof.mergeEvidence = evidence; - const pullRequest = { repository: evidence.repository, prNumber: evidence.prNumber, @@ -196,14 +220,14 @@ export async function runFullLifecycle({ const submitted = await submitAndFind({ client: contributorApi, command: buildSubmitPullRequestCommand({ - packageId, + packageName, claimedBountyCid: claimAccepted.event.contractId, pullRequest }), actAs: parties.contributor, workflowId: "commitledger-submit-pr", lookupParty: parties.contributor, - lookupTemplateId: templateId(packageId, "SubmittedBounty"), + lookupTemplateId: templateId(packageName, "SubmittedBounty"), predicate: arg => arg.bountyId === bounty.bountyId && Number(arg.pullRequest?.prNumber) === Number(prNumber) }); proof.steps.push(proofStep("PR_SUBMITTED", submitted.submission, submitted.event)); @@ -212,27 +236,28 @@ export async function runFullLifecycle({ "wrong issue evidence rejected", () => verifierApi.submitAndWait({ commands: [buildVerifyMergedCommand({ - packageId, + packageName, submittedBountyCid: submitted.event.contractId, evidence: { ...evidence, issueNumber: evidence.issueNumber + 1000 } })], actAs: [parties.verifier], workflowId: "commitledger-negative-wrong-issue", commandId: `negative-wrong-issue-${Date.now()}` - }) + }), + { codes: ["DAML_UNHANDLED_EXCEPTION", "DAML_INTERPRETATION_ERROR"], contains: "evidence issue number mismatch" } )); const verified = await submitAndFind({ client: verifierApi, command: buildVerifyMergedCommand({ - packageId, + packageName, submittedBountyCid: submitted.event.contractId, evidence }), actAs: parties.verifier, workflowId: "commitledger-verify-merge", lookupParty: parties.verifier, - lookupTemplateId: templateId(packageId, "VerifiedBounty"), + lookupTemplateId: templateId(packageName, "VerifiedBounty"), predicate: arg => arg.bountyId === bounty.bountyId && arg.evidence?.evidenceHash === evidence.evidenceHash }); proof.steps.push(proofStep("VERIFIED", verified.submission, verified.event)); @@ -243,7 +268,7 @@ export async function runFullLifecycle({ "unauthorized contributor settlement rejected", () => contributorApi.submitAndWait({ commands: [buildSettleCommand({ - packageId, + packageName, verifiedBountyCid: verified.event.contractId, settledAt, settlementRef: "unauthorized-attempt" @@ -251,13 +276,14 @@ export async function runFullLifecycle({ actAs: [parties.contributor], workflowId: "commitledger-negative-unauthorized-settle", commandId: `negative-unauthorized-settle-${Date.now()}` - }) + }), + { codes: ["DAML_AUTHORIZATION_ERROR"] } )); const settled = await submitAndFind({ client: maintainerApi, command: buildSettleCommand({ - packageId, + packageName, verifiedBountyCid: verified.event.contractId, settledAt, settlementRef @@ -265,7 +291,7 @@ export async function runFullLifecycle({ actAs: parties.maintainer, workflowId: "commitledger-settle", lookupParty: parties.maintainer, - lookupTemplateId: templateId(packageId, "SettlementReceipt"), + lookupTemplateId: templateId(packageName, "SettlementReceipt"), predicate: arg => arg.bountyId === bounty.bountyId && arg.evidenceHash === evidence.evidenceHash }); proof.steps.push(proofStep("SETTLED", settled.submission, settled.event)); @@ -275,7 +301,7 @@ export async function runFullLifecycle({ "duplicate settlement rejected", () => maintainerApi.submitAndWait({ commands: [buildSettleCommand({ - packageId, + packageName, verifiedBountyCid: verified.event.contractId, settledAt, settlementRef: "duplicate-attempt" @@ -283,7 +309,8 @@ export async function runFullLifecycle({ actAs: [parties.maintainer], workflowId: "commitledger-negative-duplicate-settle", commandId: `negative-duplicate-settle-${Date.now()}` - }) + }), + { codes: ["CONTRACT_NOT_FOUND", "CONTRACT_NOT_ACTIVE"], contains: verified.event.contractId } )); return proof; diff --git a/src/run-full-demo.mjs b/src/run-full-demo.mjs index 6f584d7..51c84d4 100644 --- a/src/run-full-demo.mjs +++ b/src/run-full-demo.mjs @@ -1,31 +1,20 @@ -import { writeFile } from "node:fs/promises"; -import { runtimeConfigFromEnv, runFullLifecycle } from "./orchestrator.mjs"; - -const issueUrl = process.env.COMMITLEDGER_ISSUE_URL || "https://github.com/Saidur-droid/CommitLedger/issues/5"; -const prNumber = Number(process.env.COMMITLEDGER_PR_NUMBER || "6"); -const contributorGithub = process.env.COMMITLEDGER_CONTRIBUTOR_GITHUB || "Saidur-droid"; -const rewardAmount = Number(process.env.COMMITLEDGER_REWARD_AMOUNT || "100"); -const baseBranch = process.env.COMMITLEDGER_BASE_BRANCH || "main"; - +import {writeFile} from 'node:fs/promises'; +import {runtimeConfigFromEnv,runFullLifecycle} from './orchestrator.mjs'; try { - const proof = await runFullLifecycle({ - runtime: runtimeConfigFromEnv(), - issueUrl, - rewardAmount, - contributorGithub, - prNumber, - baseBranch, - githubToken: process.env.GITHUB_TOKEN || "" + const proof=await runFullLifecycle({ + runtime:runtimeConfigFromEnv(), + issueUrl:process.env.COMMITLEDGER_ISSUE_URL || 'https://github.com/Saidur-droid/CommitLedger/issues/5', + prNumber:Number(process.env.COMMITLEDGER_PR_NUMBER || 6), + contributorGithub:process.env.COMMITLEDGER_CONTRIBUTOR_GITHUB || 'Saidur-droid', + rewardAmount:Number(process.env.COMMITLEDGER_REWARD_AMOUNT || 100), + baseBranch:process.env.COMMITLEDGER_BASE_BRANCH || 'main', + githubToken:process.env.GITHUB_TOKEN || '' }); - - const json = JSON.stringify(proof, null, 2); + proof.sourceCommit=process.env.GITHUB_SHA || 'local-working-tree'; + const json=JSON.stringify(proof,null,2)+'\n'; console.log(json); - - if (process.env.COMMITLEDGER_EVIDENCE_FILE) { - await writeFile(process.env.COMMITLEDGER_EVIDENCE_FILE, json + "\n", "utf8"); - console.error(`Evidence written to ${process.env.COMMITLEDGER_EVIDENCE_FILE}`); - } -} catch (error) { + if(process.env.COMMITLEDGER_EVIDENCE_FILE) await writeFile(process.env.COMMITLEDGER_EVIDENCE_FILE,json); +} catch(error) { console.error(`CommitLedger full demo failed: ${error.message}`); - process.exit(1); + process.exitCode=1; } diff --git a/src/server.mjs b/src/server.mjs index b8e4388..dd97f38 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -1,204 +1,121 @@ -import http from "node:http"; -import { readFile } from "node:fs/promises"; -import { extname, join, normalize } from "node:path"; -import { fileURLToPath } from "node:url"; -import { fetchGitHubIssue, fetchVerifiedPullRequest } from "./github-verifier.mjs"; -import { validateBountyDraft } from "./domain.mjs"; -import { - buildCreateBountyCommand, - buildClaimRequestCommand, - buildAcceptClaimCommand, - buildSubmitPullRequestCommand, - buildReturnForRevisionCommand, - buildVerifyMergedCommand, - buildSettleCommand -} from "./canton-commands.mjs"; -import { CantonJsonApi } from "./canton-json-api.mjs"; -import { runtimeConfigFromEnv, runFullLifecycle } from "./orchestrator.mjs"; - -const root = fileURLToPath(new URL("../web/", import.meta.url)); -const port = Number(process.env.PORT || 4173); - -function json(res, status, payload) { - const body = JSON.stringify(payload, null, 2); - res.writeHead(status, { - "Content-Type": "application/json; charset=utf-8", - "Content-Length": Buffer.byteLength(body), - "Cache-Control": "no-store" - }); - res.end(body); +import http from 'node:http'; +import {readFile} from 'node:fs/promises'; +import {extname,resolve,sep} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {fetchGitHubIssue,fetchVerifiedPullRequest} from './github-verifier.mjs'; +import {validateBountyDraft} from './domain.mjs'; +import * as builders from './canton-commands.mjs'; +import {CantonJsonApi} from './canton-json-api.mjs'; +import {runtimeConfigFromEnv,runFullLifecycle} from './orchestrator.mjs'; +const projectRoot=fileURLToPath(new URL('../',import.meta.url)); +const root=resolve(projectRoot,'web'); +const evidenceRoot=resolve(projectRoot,'evidence'); +const actions=Object.freeze({createBounty:'buildCreateBountyCommand',claimRequest:'buildClaimRequestCommand',acceptClaim:'buildAcceptClaimCommand',submitPullRequest:'buildSubmitPullRequestCommand',returnForRevision:'buildReturnForRevisionCommand',verifyMerged:'buildVerifyMergedCommand',settle:'buildSettleCommand'}); +function json(res,status,payload) { + res.writeHead(status,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}); + res.end(JSON.stringify(payload,null,2)); } - async function readJson(req) { - let body = ""; - for await (const chunk of req) { - body += chunk; - if (body.length > 1_000_000) throw new Error("request body too large"); - } - return body ? JSON.parse(body) : {}; -} - -function contentType(path) { - return ({ - ".html": "text/html; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".svg": "image/svg+xml" - })[extname(path)] || "application/octet-stream"; + let length=0;const chunks=[]; + for await(const chunk of req) {length+=chunk.length;if(length>1_000_000) throw new Error('request body too large');chunks.push(chunk);} + const value=JSON.parse(Buffer.concat(chunks).toString('utf8')||'{}'); + if(!value || typeof value!=='object' || Array.isArray(value)) throw new Error('invalid JSON object'); + return value; } - -async function serveStatic(urlPath, res) { - const clean = normalize(urlPath === "/" ? "/index.html" : urlPath).replace(/^([.][.][/\\])+/, ""); - const target = join(root, clean); - if (!target.startsWith(root)) return json(res, 403, { error: "forbidden" }); - try { - const body = await readFile(target); - res.writeHead(200, { "Content-Type": contentType(target), "Cache-Control": "no-store" }); - res.end(body); - } catch { - json(res, 404, { error: "not found" }); +async function readVerifiedProof() { + const [proofRaw,verificationRaw]=await Promise.all([ + readFile(resolve(evidenceRoot,'canton-proof.json'),'utf8'), + readFile(resolve(evidenceRoot,'verification.json'),'utf8') + ]); + const proof=JSON.parse(proofRaw); + const verification=JSON.parse(verificationRaw); + if(!/^[a-f0-9]{40}$/.test(proof.sourceCommit||'') || proof.sourceCommit!==verification.commit) { + throw new Error('deployed proof artifacts are not source-commit coherent'); } -} - -function cantonConfigured() { - return Boolean(process.env.CANTON_JSON_API_URL && (process.env.CANTON_TOKEN || process.env.CANTON_MAINTAINER_TOKEN)); -} - -function lifecycleConfigured() { - try { - runtimeConfigFromEnv(); - return true; - } catch { - return false; + if(proof.steps?.length!==6 || proof.negativeChecks?.length!==3 || !proof.settlementReceipt) { + throw new Error('deployed proof artifacts are incomplete'); } + return {proof,verification}; } - -function cantonApi() { - const token = process.env.CANTON_TOKEN || process.env.CANTON_MAINTAINER_TOKEN; - if (!process.env.CANTON_JSON_API_URL || !token) return null; - return new CantonJsonApi({ baseUrl: process.env.CANTON_JSON_API_URL, token }); -} - -async function submitCanton(command, actAs, workflowId) { - const api = cantonApi(); - if (!api) throw new Error("Canton LocalNet is not configured"); - return api.submitAndWait({ - commands: [command], - actAs, - workflowId, - commandId: `commitledger-${workflowId}-${Date.now()}` - }); -} - -const server = http.createServer(async (req, res) => { - try { - const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); - - if (req.method === "GET" && url.pathname === "/api/health") { - return json(res, 200, { - ok: true, - service: "CommitLedger", - cantonConfigured: cantonConfigured(), - lifecycleConfigured: lifecycleConfigured(), - githubAuthenticated: Boolean(process.env.GITHUB_TOKEN), - packageConfigured: Boolean(process.env.CANTON_PACKAGE_ID) - }); - } - - if (req.method === "POST" && url.pathname === "/api/bounty/prepare") { - const input = await readJson(req); - const issue = await fetchGitHubIssue({ issueUrl: input.issueUrl, token: process.env.GITHUB_TOKEN }); - const bounty = validateBountyDraft({ - repository: issue.repository, - issueNumber: issue.issueNumber, - issueUrl: issue.issueUrl, - title: input.title || issue.title, - rewardAmount: input.rewardAmount, - rewardUnit: "DEMO_CREDIT" - }); - return json(res, 200, { ok: true, issue, bounty }); - } - - if (req.method === "POST" && url.pathname === "/api/github/verify") { - const input = await readJson(req); - const evidence = await fetchVerifiedPullRequest({ - repository: input.repository, - prNumber: Number(input.prNumber), - token: process.env.GITHUB_TOKEN, - expected: { - headSha: input.headSha || "", - baseBranch: input.baseBranch || "", - contributorGithub: input.contributorGithub || "", - issueNumber: Number(input.issueNumber) +export function createAppServer(env=process.env) { + return http.createServer(async(req,res)=>{ + try { + const forwardedProto=String(req.headers['x-forwarded-proto']||'').split(',')[0].trim().toLowerCase(); + const protocol=['http','https'].includes(forwardedProto)?forwardedProto:'http'; + const origin=`${protocol}://${req.headers.host||'localhost'}`; + const url=new URL(req.url,origin); + const allowedHosts=new Set(['127.0.0.1','localhost','[::1]']); + if(env.COMMITLEDGER_PUBLIC_HOST) allowedHosts.add(String(env.COMMITLEDGER_PUBLIC_HOST).trim().toLowerCase()); + if(!allowedHosts.has(url.hostname.toLowerCase())) return json(res,403,{error:'Untrusted host'}); + if(req.method==='POST') { + if(req.headers.origin && req.headers.origin!==origin) return json(res,403,{error:'Cross-origin write rejected'}); + if(!/^application\/json(?:;|$)/i.test(req.headers['content-type']||'')) return json(res,415,{error:'application/json is required'}); + } + if(req.method==='GET' && url.pathname==='/api/proof') { + try { + const {proof,verification}=await readVerifiedProof(); + return json(res,200,{ok:true,proof,verification}); + } catch { + return json(res,503,{error:'Verified deployment proof is not available'}); } - }); - return json(res, 200, { ok: true, evidence }); - } - - if (req.method === "POST" && url.pathname === "/api/demo/run") { - if (!lifecycleConfigured()) { - return json(res, 503, { error: "Full Canton lifecycle is not configured. Set package, party and token environment variables." }); } - const input = await readJson(req); - const proof = await runFullLifecycle({ - runtime: runtimeConfigFromEnv(), - issueUrl: input.issueUrl, - rewardAmount: Number(input.rewardAmount || 100), - contributorGithub: input.contributorGithub, - prNumber: Number(input.prNumber), - baseBranch: input.baseBranch || "main", - githubToken: process.env.GITHUB_TOKEN || "" - }); - return json(res, 200, { ok: true, proof }); - } - - if (req.method === "POST" && url.pathname === "/api/canton/build-command") { - const input = await readJson(req); - const packageId = input.packageId || process.env.CANTON_PACKAGE_ID; - if (!packageId) throw new Error("CANTON_PACKAGE_ID is required"); - let command; - switch (input.action) { - case "createBounty": - command = buildCreateBountyCommand({ packageId, ...input.payload }); - break; - case "claimRequest": - command = buildClaimRequestCommand({ packageId, ...input.payload }); - break; - case "acceptClaim": - command = buildAcceptClaimCommand({ packageId, ...input.payload }); - break; - case "submitPullRequest": - command = buildSubmitPullRequestCommand({ packageId, ...input.payload }); - break; - case "returnForRevision": - command = buildReturnForRevisionCommand({ packageId, ...input.payload }); - break; - case "verifyMerged": - command = buildVerifyMergedCommand({ packageId, ...input.payload }); - break; - case "settle": - command = buildSettleCommand({ packageId, ...input.payload }); - break; - default: - throw new Error("Unknown Canton action"); + if(req.method==='GET' && url.pathname==='/api/health') { + let runtime;let configurationError='';let ledgerReachable=false; + try {runtime=runtimeConfigFromEnv(env);} catch(error) {configurationError=error.message;} + if(runtime) { + try { + const api=new CantonJsonApi({baseUrl:runtime.baseUrl,token:runtime.tokens.maintainer,userId:runtime.userId,insecureLocal:runtime.insecureLocal}); + await api.request('/v2/state/ledger-end',{signal:AbortSignal.timeout(3000)}); + ledgerReachable=true; + } catch { /* Connectivity is separate from configuration and ledger proof. */ } + } + let verifiedProofAvailable=false; + try {await readVerifiedProof();verifiedProofAvailable=true;} catch { /* Build proof is optional outside verified deployments. */ } + return json(res,200,{ok:true,service:'CommitLedger',lifecycleConfigured:Boolean(runtime),cantonConfigured:Boolean(runtime),ledgerReachable,verifiedProofAvailable,configurationError,githubAuthenticated:Boolean(env.GITHUB_TOKEN),packageConfigured:Boolean(env.CANTON_PACKAGE_ID)}); } - return json(res, 200, { ok: true, command }); - } - - if (req.method === "POST" && url.pathname === "/api/canton/submit-command") { - const input = await readJson(req); - const result = await submitCanton(input.command, input.actAs, input.workflowId || "web"); - return json(res, 200, { ok: true, result }); + if(req.method==='POST' && url.pathname==='/api/canton/submit-command') { + return json(res,403,{error:'Arbitrary browser-supplied ledger commands are disabled; use the verified lifecycle'}); + } + if(req.method==='POST' && url.pathname==='/api/bounty/prepare') { + const input=await readJson(req); + const issue=await fetchGitHubIssue({issueUrl:input.issueUrl,token:env.GITHUB_TOKEN}); + const bounty=validateBountyDraft({...issue,rewardAmount:input.rewardAmount,rewardUnit:'DEMO_CREDIT'}); + return json(res,200,{ok:true,issue,bounty}); + } + if(req.method==='POST' && url.pathname==='/api/github/verify') { + const input=await readJson(req); + const evidence=await fetchVerifiedPullRequest({repository:input.repository,prNumber:Number(input.prNumber),token:env.GITHUB_TOKEN,expected:{headSha:input.headSha||'',mergeCommitSha:input.mergeCommitSha||'',baseBranch:input.baseBranch||'main',contributorGithub:input.contributorGithub||'',issueNumber:Number(input.issueNumber)}}); + return json(res,200,{ok:true,evidence}); + } + if(req.method==='POST' && url.pathname==='/api/demo/run') { + let runtime; + try {runtime=runtimeConfigFromEnv(env);} catch(error) {return json(res,503,{error:`Canton setup pending: ${error.message}`});} + const input=await readJson(req); + const proof=await runFullLifecycle({runtime,issueUrl:input.issueUrl,rewardAmount:Number(input.rewardAmount||100),contributorGithub:input.contributorGithub,prNumber:Number(input.prNumber),baseBranch:input.baseBranch||'main',githubToken:env.GITHUB_TOKEN||''}); + return json(res,200,{ok:true,proof}); + } + if(req.method==='POST' && url.pathname==='/api/canton/build-command') { + const input=await readJson(req); + const builder=Object.hasOwn(actions,input.action) ? builders[actions[input.action]] : null; + if(typeof builder!=='function') return json(res,400,{error:'Unknown Canton action'}); + const command=builder({...input.payload,packageId:env.CANTON_PACKAGE_ID||input.packageId}); + return json(res,200,{ok:true,command}); + } + if(req.method!=='GET') return json(res,405,{error:'method not allowed'}); + const target=resolve(root,'.'+(url.pathname==='/'?'/index.html':url.pathname)); + if(!target.startsWith(resolve(root)+sep)) return json(res,403,{error:'forbidden'}); + try { + const body=await readFile(target); + res.writeHead(200,{'Content-Type':({'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml'})[extname(target)]||'application/octet-stream','Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}); + res.end(body); + } catch {json(res,404,{error:'not found'});} + } catch(error) { + json(res,/required|invalid|mismatch|must|Unknown|distinct|JSON|too large/.test(error.message)?400:502,{error:error.message}); } - - if (req.method === "GET") return serveStatic(url.pathname, res); - json(res, 405, { error: "method not allowed" }); - } catch (error) { - const status = /not configured|required|invalid|mismatch|must|Unknown|distinct/.test(error.message) ? 400 : 500; - json(res, status, { error: error.message }); - } -}); - -server.listen(port, "127.0.0.1", () => { - console.log(`CommitLedger running at http://127.0.0.1:${port}`); -}); + }); +} +if(process.argv[1] && resolve(process.argv[1])===fileURLToPath(import.meta.url)) { + const port=Number(process.env.PORT||4173); + const host=process.env.COMMITLEDGER_BIND_HOST||'127.0.0.1'; + createAppServer().listen(port,host,()=>console.log(`CommitLedger running at http://${host}:${port}`)); +} diff --git a/tests/bootstrap-local-static.test.mjs b/tests/bootstrap-local-static.test.mjs new file mode 100644 index 0000000..68a5901 --- /dev/null +++ b/tests/bootstrap-local-static.test.mjs @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const source = await fs.readFile(new URL('../scripts/bootstrap-local.mjs', import.meta.url), 'utf8'); + +test('party bootstrap retries only the known synchronizer connection race', () => { + assert.match(source, /PARTY_ALLOCATION_WITHOUT_CONNECTED_SYNCHRONIZER/); + assert.match(source, /attempt<=30/); + assert.match(source, /await delay\(1000\)/); + assert.doesNotMatch(source, /catch\s*\([^)]*\)\s*\{\s*await delay/); +}); diff --git a/tests/canton-commands.test.mjs b/tests/canton-commands.test.mjs index 4d6bb46..a533854 100644 --- a/tests/canton-commands.test.mjs +++ b/tests/canton-commands.test.mjs @@ -2,14 +2,15 @@ import test from "node:test"; import assert from "node:assert/strict"; import { buildCreateBountyCommand, + buildSubmitPullRequestCommand, buildReturnForRevisionCommand, buildVerifyMergedCommand, buildSettleCommand } from "../src/canton-commands.mjs"; -test("builds a Daml Bounty create command with the exact template", () => { +test("builds a Daml Bounty create command with Int fields encoded as strings", () => { const command = buildCreateBountyCommand({ - packageId: "pkg123", + packageName: "commit-ledger", maintainer: "Maintainer::1", verifier: "Verifier::1", bounty: { @@ -22,13 +23,29 @@ test("builds a Daml Bounty create command with the exact template", () => { rewardUnit: "DEMO_CREDIT" } }); - assert.equal(command.CreateCommand.templateId, "pkg123:CommitLedger:Bounty"); + assert.equal(command.CreateCommand.templateId, "#commit-ledger:CommitLedger:Bounty"); + assert.equal(command.CreateCommand.createArguments.issueNumber, "5"); assert.equal(command.CreateCommand.createArguments.rewardUnit, "DEMO_CREDIT"); }); +test("encodes nested pull-request Int fields as strings for Daml values", () => { + const command = buildSubmitPullRequestCommand({ + packageName: "commit-ledger", + claimedBountyCid: "cid-claimed", + pullRequest: { + repository: "Saidur-droid/CommitLedger", + prNumber: 6, + prUrl: "https://github.com/Saidur-droid/CommitLedger/pull/6", + headSha: "abc", + baseBranch: "main" + } + }); + assert.equal(command.ExerciseCommand.choiceArgument.pullRequest.prNumber, "6"); +}); + test("builds the explicit revision path", () => { const command = buildReturnForRevisionCommand({ - packageId: "pkg123", + packageName: "commit-ledger", submittedBountyCid: "cid-submitted", reason: "Update tests" }); @@ -36,30 +53,32 @@ test("builds the explicit revision path", () => { assert.equal(command.ExerciseCommand.choiceArgument.reason, "Update tests"); }); -test("binds issue-aware canonical merge evidence into the verifier choice", () => { +test("binds issue-aware canonical merge evidence with Daml Int strings", () => { const evidence = { repository: "Saidur-droid/CommitLedger", issueNumber: 5, prNumber: 6, prUrl: "https://github.com/Saidur-droid/CommitLedger/pull/6", headSha: "abc", + mergeCommitSha: "b".repeat(40), baseBranch: "main", merged: true, mergedAt: "2026-09-27T10:57:30Z", evidenceHash: "sha256:abc" }; const command = buildVerifyMergedCommand({ - packageId: "pkg123", + packageName: "commit-ledger", submittedBountyCid: "cid-submitted", evidence }); assert.equal(command.ExerciseCommand.choice, "SubmittedBounty_VerifyMerged"); - assert.equal(command.ExerciseCommand.choiceArgument.evidence.issueNumber, 5); + assert.equal(command.ExerciseCommand.choiceArgument.evidence.issueNumber, "5"); + assert.equal(command.ExerciseCommand.choiceArgument.evidence.prNumber, "6"); }); test("settlement is an explicit consuming Daml choice", () => { const command = buildSettleCommand({ - packageId: "pkg123", + packageName: "commit-ledger", verifiedBountyCid: "cid-verified", settledAt: "2026-09-27T10:00:00Z", settlementRef: "settlement-001" diff --git a/tests/canton-json-api.test.mjs b/tests/canton-json-api.test.mjs index a4c77e9..53d04fb 100644 --- a/tests/canton-json-api.test.mjs +++ b/tests/canton-json-api.test.mjs @@ -1,11 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { eventFormatForParty, extractCreatedEvents } from "../src/canton-json-api.mjs"; +import { eventFormatForParty, extractCreatedEvents, extractTransactionCreatedEvents } from "../src/canton-json-api.mjs"; test("builds a template-scoped Canton active-contract filter", () => { - const format = eventFormatForParty("Maintainer::1", "pkg:CommitLedger:Bounty"); + const format = eventFormatForParty("Maintainer::1", "#commit-ledger:CommitLedger:Bounty"); const filter = format.filtersByParty["Maintainer::1"].cumulative[0].identifierFilter.TemplateFilter.value; - assert.equal(filter.templateId, "pkg:CommitLedger:Bounty"); + assert.equal(filter.templateId, "#commit-ledger:CommitLedger:Bounty"); assert.equal(filter.includeCreatedEventBlob, false); assert.equal(format.verbose, true); }); @@ -17,7 +17,7 @@ test("extracts active created events from Canton JSON API responses", () => { JsActiveContract: { createdEvent: { contractId: "00abc", - templateId: "pkg:CommitLedger:Bounty", + templateId: "#commit-ledger:CommitLedger:Bounty", createArgument: { bountyId: "bounty-5" } } } @@ -29,3 +29,21 @@ test("extracts active created events from Canton JSON API responses", () => { assert.equal(events[0].contractId, "00abc"); assert.equal(events[0].createArgument.bountyId, "bounty-5"); }); + + +test("extracts created events from submit-and-wait transaction responses", () => { + const events = extractTransactionCreatedEvents({ + transaction: { + updateId: "unit-update", + offset: 1, + events: [{ CreatedEvent: { + contractId: "00created", + templateId: "a".repeat(64) + ":CommitLedger:Bounty", + createArgument: { bountyId: "bounty-5" } + } }] + } + }); + assert.equal(events.length, 1); + assert.equal(events[0].contractId, "00created"); + assert.equal(events[0].createArgument.bountyId, "bounty-5"); +}); diff --git a/tests/competition-readiness.test.mjs b/tests/competition-readiness.test.mjs new file mode 100644 index 0000000..ab2c7f7 --- /dev/null +++ b/tests/competition-readiness.test.mjs @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { evaluateCompetitionReadiness } from "../src/competition-readiness.mjs"; + +const verifiedImplementation = { + nodeTests: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + damlBuild: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + damlTests: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + cantonLifecycle: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + negativeSecurity: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + judgeUi: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, + track1Package: { status: "verified", runId: "run-1", sourceCommit: "abc123" }, +}; +const verifiedExternal = { + publicRepository: { status: "verified" }, + publicArtifacts: { status: "verified" }, + mana1000: { status: "verified" }, + activity10Days: { status: "verified" }, + journalNonEmpty: { status: "verified" }, + projectProfileComplete: { status: "verified" }, + track1Selected: { status: "verified" }, +}; + +test("returns PASS only when implementation and external gates are verified", () => { + const result = evaluateCompetitionReadiness({ implementation: verifiedImplementation, external: verifiedExternal }); + assert.equal(result.status, "PASS"); + assert.deepEqual(result.counts, { pass: 14, blocked: 0, fail: 0 }); +}); + +test("returns BLOCKED when an external gate is not yet verified", () => { + const result = evaluateCompetitionReadiness({ + implementation: verifiedImplementation, + external: { ...verifiedExternal, mana1000: { status: "blocked", reason: "AppsFactory account not checked" } }, + }); + assert.equal(result.status, "BLOCKED"); + assert.equal(result.gates.find(g => g.name === "mana1000").status, "BLOCKED"); +}); + +test("returns FAIL when an implementation gate fails even if external gates are blocked", () => { + const result = evaluateCompetitionReadiness({ + implementation: { ...verifiedImplementation, damlTests: { status: "failed", reason: "Daml test failed" } }, + external: { ...verifiedExternal, publicRepository: { status: "blocked" } }, + }); + assert.equal(result.status, "FAIL"); + assert.ok(result.counts.fail >= 1); +}); + +test("rejects evidence mixed across runtime run IDs or source commits", () => { + const result = evaluateCompetitionReadiness({ + implementation: { + ...verifiedImplementation, + cantonLifecycle: { status: "verified", runId: "run-2", sourceCommit: "def456" }, + }, + external: verifiedExternal, + }); + assert.equal(result.status, "FAIL"); + assert.equal(result.gates.find(g => g.name === "evidenceCoherence").status, "FAIL"); +}); diff --git a/tests/evidence-coherence-static.test.mjs b/tests/evidence-coherence-static.test.mjs new file mode 100644 index 0000000..f012a6c --- /dev/null +++ b/tests/evidence-coherence-static.test.mjs @@ -0,0 +1,16 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const checker = await fs.readFile(new URL('../scripts/check-evidence.mjs', import.meta.url), 'utf8'); +const verifier = await fs.readFile(new URL('../scripts/verify-all.mjs', import.meta.url), 'utf8'); +const runner = await fs.readFile(new URL('../scripts/run-local-proof.sh', import.meta.url), 'utf8'); + +test('verification and Canton proof are forced onto the exact checked-out commit', () => { + assert.match(verifier, /spawn\("git", \["rev-parse", "HEAD"\]/); + assert.match(verifier, /const source = await new Promise/); + assert.match(runner, /SOURCE_COMMIT=\$\(git rev-parse HEAD\)/); + assert.match(runner, /export GITHUB_SHA="\$SOURCE_COMMIT"/); + assert.match(checker, /proof\.sourceCommit!==verification\.commit/); + assert.match(checker, /\^\[a-f0-9\]\{40\}\$/); +}); diff --git a/tests/github-verifier.test.mjs b/tests/github-verifier.test.mjs index 0b9c92d..a0db588 100644 --- a/tests/github-verifier.test.mjs +++ b/tests/github-verifier.test.mjs @@ -13,9 +13,10 @@ const payload = { html_url: "https://github.com/Saidur-droid/CommitLedger/pull/6", body: "Competition/demo evidence for #5.", merged: true, + merge_commit_sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", merged_at: "2026-09-27T10:57:30Z", user: { login: "Saidur-droid" }, - head: { sha: "abc123" }, + head: { sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, base: { ref: "main", repo: { full_name: "Saidur-droid/CommitLedger" } } }; @@ -51,7 +52,7 @@ test("binds the exact bounty issue into canonical merge evidence", () => { assertExpectedPullRequest(pr, { repository: "saidur-droid/commitledger", prNumber: 6, - headSha: "abc123", + headSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", baseBranch: "main", contributorGithub: "Saidur-droid", issueNumber: 5 diff --git a/tests/judge-ui-static.test.mjs b/tests/judge-ui-static.test.mjs new file mode 100644 index 0000000..f4a5c2f --- /dev/null +++ b/tests/judge-ui-static.test.mjs @@ -0,0 +1,31 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const html = await fs.readFile(new URL('../web/index.html', import.meta.url), 'utf8'); + +test('judge UI exposes competition readiness without claiming submission ready', () => { + assert.match(html, /id="competition-readiness"/); + assert.match(html, /BLOCKED · external gates pending/); + assert.doesNotMatch(html, /SUBMISSION READY/); +}); + +test('judge UI maps all six official judging criteria', () => { + for (const label of ['Value \/ Problem','ICP \/ Audience','Metrics \/ Validation','GTM Materials','MVP Materials','Pitch Materials']) { + assert.match(html, new RegExp(label)); + } +}); + +test('judge UI identifies Track 1 and describes proof as evidence-first', () => { + assert.match(html, /Track 1/); + assert.match(html, /no sample transaction IDs or simulated settlement success/i); +}); + +test('judge cockpit exposes current verified fixture and runtime proof state', () => { + for (const label of ['Source','Authorization','Settlement','Runtime']) { + assert.match(html, new RegExp(label)); + } + assert.match(html, /GitHub #69 → PR #73/); + assert.match(html, /Runtime[\s\S]*VERIFIED/); + assert.match(html, /PASS[\s\S]*Daml \+ real Canton proof/); +}); diff --git a/tests/lifecycle-transport.test.mjs b/tests/lifecycle-transport.test.mjs new file mode 100644 index 0000000..d689f2e --- /dev/null +++ b/tests/lifecycle-transport.test.mjs @@ -0,0 +1,91 @@ +// Simulated HTTP transport tests. These are NOT Canton runtime evidence. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {runFullLifecycle} from '../src/orchestrator.mjs'; +const repository='Saidur-droid/CommitLedger'; +const parties={maintainer:'Maintainer::unit',contributor:'Contributor::unit',verifier:'Verifier::unit'}; +const runtime={baseUrl:'http://localhost:3975',packageId:'a'.repeat(64),packageName:'commit-ledger',parties,tokens:{maintainer:'unit-maintainer',contributor:'unit-contributor',verifier:'unit-verifier'}}; +const input={runtime,issueUrl:`https://github.com/${repository}/issues/5`,contributorGithub:'Saidur-droid',prNumber:6}; +function installTransport(t, {unmerged=false,negativeNetworkFailure=false}={}) { + const active=new Map(); const commands=[]; let seq=0; + t.mock.method(globalThis,'fetch',async (url,options={}) => { + if(url.startsWith('https://api.github.com/')) { + if(url.includes('/issues/')) return Response.json({number:5,html_url:input.issueUrl,title:'Unit fixture',state:'open',user:{login:'Saidur-droid'}}); + if(url.includes('/pulls/')) return Response.json({number:6,html_url:`https://github.com/${repository}/pull/6`,body:'References #5',merged:!unmerged,merged_at:unmerged?null:'2026-09-27T10:57:30Z',merge_commit_sha:'b'.repeat(40),head:{sha:'a'.repeat(40)},base:{ref:'main',repo:{full_name:repository}},user:{login:'Saidur-droid'}}); + if(url.includes('/commits/')) return Response.json({sha:'b'.repeat(40)}); + if(url.includes('/compare/')) return Response.json({status:'ahead'}); + throw new Error(`Unexpected GitHub URL: ${url}`); + } + const body=JSON.parse(options.body); + const transactionMode=url.endsWith('/v2/commands/submit-and-wait-for-transaction'); + assert.ok(transactionMode || url.endsWith('/v2/commands/submit-and-wait')); + const payload=transactionMode ? body.commands : body; + commands.push(payload); + const command=payload.commands[0]; let name,args; + if(command.CreateCommand) { + name=command.CreateCommand.templateId.split(':').at(-1); + args=command.CreateCommand.createArguments; + } else { + const ex=command.ExerciseCommand; + const previous=active.get(ex.contractId); + if(!previous) return Response.json({code:'CONTRACT_NOT_FOUND',cause:`Contract ${ex.contractId} already consumed`},{status:404}); + args={...previous.createArgument}; + if(ex.choice==='ClaimRequest_Accept') { + assert.equal(payload.actAs[0],parties.maintainer); + const bounty=active.get(ex.choiceArgument.bountyCid); + assert.ok(bounty,'exact prior bounty CID must be carried forward'); + args={...bounty.createArgument,contributor:args.contributor,contributorGithub:args.contributorGithub}; + active.delete(ex.choiceArgument.bountyCid); name='ClaimedBounty'; + } else if(ex.choice==='ClaimedBounty_SubmitPullRequest') { + assert.equal(payload.actAs[0],parties.contributor); + args.pullRequest=ex.choiceArgument.pullRequest; name='SubmittedBounty'; + } else if(ex.choice==='SubmittedBounty_VerifyMerged') { + assert.equal(payload.actAs[0],parties.verifier); + if(ex.choiceArgument.evidence.issueNumber!==args.issueNumber) { + if(negativeNetworkFailure) throw new TypeError('fetch failed'); + return Response.json({code:'DAML_UNHANDLED_EXCEPTION',cause:'evidence issue number mismatch'},{status:400}); + } + args.evidence=ex.choiceArgument.evidence;name='VerifiedBounty'; + } else if(ex.choice==='VerifiedBounty_Settle') { + if(payload.actAs[0]!==parties.maintainer) return Response.json({code:'DAML_AUTHORIZATION_ERROR',cause:'wrong controller'},{status:400}); + args={...args,...ex.choiceArgument,evidenceHash:args.evidence.evidenceHash,mergeCommitSha:args.evidence.mergeCommitSha};name='SettlementReceipt'; + } else throw new Error(`Unexpected choice ${ex.choice}`); + active.delete(ex.contractId); + } + seq++; + const event={contractId:`unit-contract-${seq}`,templateId:`${'a'.repeat(64)}:CommitLedger:${name}`,createArgument:args}; + active.set(event.contractId,event); + if(transactionMode) return Response.json({transaction:{updateId:`unit-update-${seq}`,offset:seq,events:[{CreatedEvent:event}]}}); + return Response.json({updateId:`unit-update-${seq}`,completionOffset:seq}); + }); + return {commands,active}; +} +test('UNIT TRANSPORT: six transitions chain exact contract IDs and capture three specific rejections',async t=>{ + const {commands,active}=installTransport(t); + const proof=await runFullLifecycle(input); + assert.equal(proof.steps.length,6); + assert.equal(proof.negativeChecks.length,3); + assert.equal(commands.length,9); + assert.equal(active.size,1); + assert.equal(proof.settlementReceipt.evidenceHash,proof.mergeEvidence.evidenceHash); + assert.equal(proof.settlementReceipt.mergeCommitSha,'b'.repeat(40)); + assert.deepEqual(proof.steps.map(x=>x.contractId),Array.from({length:6},(_,i)=>`unit-contract-${i+1}`)); + assert.deepEqual(proof.negativeChecks.map(x=>x.code),['DAML_UNHANDLED_EXCEPTION','DAML_AUTHORIZATION_ERROR','CONTRACT_NOT_FOUND']); +}); +test('UNIT TRANSPORT: invalid PR is rejected before ANY ledger mutation',async t=>{ + const {commands}=installTransport(t,{unmerged:true}); + await assert.rejects(runFullLifecycle(input),/not merged/); + assert.equal(commands.length,0); +}); +test('UNIT TRANSPORT: a network failure during a negative check aborts, never becomes proof',async t=>{ + const {commands}=installTransport(t,{negativeNetworkFailure:true}); + await assert.rejects(runFullLifecycle(input),/Unproven ledger rejection/); + assert.equal(commands.length,5); +}); +test('UNIT TRANSPORT: repeat demos use distinct bounty identities',async t=>{ + installTransport(t); + const a=await runFullLifecycle(input); + const b=await runFullLifecycle(input); + assert.notEqual(a.bounty.bountyId,b.bounty.bountyId); + assert.notEqual(a.runId,b.runId); +}); diff --git a/tests/orchestrator.test.mjs b/tests/orchestrator.test.mjs index a5ff97d..a833afb 100644 --- a/tests/orchestrator.test.mjs +++ b/tests/orchestrator.test.mjs @@ -22,6 +22,7 @@ test("runtime config supports one local token or role-specific tokens", () => { CANTON_CONTRIBUTOR_PARTY: "Contributor::1", CANTON_VERIFIER_PARTY: "Verifier::1" }); + assert.equal(config.packageName, "commit-ledger"); assert.equal(config.tokens.maintainer, "local-admin-token"); assert.equal(config.tokens.contributor, "local-admin-token"); assert.equal(config.tokens.verifier, "local-admin-token"); diff --git a/tests/proof-wrapper-static.test.mjs b/tests/proof-wrapper-static.test.mjs new file mode 100644 index 0000000..5fe386f --- /dev/null +++ b/tests/proof-wrapper-static.test.mjs @@ -0,0 +1,45 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const wrapper = await fs.readFile(new URL('../scripts/prove-and-report.sh', import.meta.url), 'utf8'); +const shellVerify = await fs.readFile(new URL('../scripts/verify-all.sh', import.meta.url), 'utf8'); +const nodeVerify = await fs.readFile(new URL('../scripts/verify-all.mjs', import.meta.url), 'utf8'); +const runner = await fs.readFile(new URL('../scripts/run-local-proof.sh', import.meta.url), 'utf8'); + +test('single proof wrapper always reports exit status and useful diagnostics', () => { + assert.match(wrapper, /EXIT STATUS/); + assert.match(wrapper, /canton-demo\.log/); + assert.match(wrapper, /canton-sandbox\.log/); + assert.match(wrapper, /daml-tests\.log/); + assert.match(wrapper, /node-tests\.log/); +}); + +test('shell verifier is only a compatibility exec wrapper', () => { + assert.match(shellVerify, /exec node scripts\/verify-all\.mjs/); + assert.doesNotMatch(shellVerify, /npm test/); + assert.doesNotMatch(shellVerify, /dpm build/); +}); + +test('Node verifier owns all four deterministic verification stages', () => { + for (const marker of ['STAGE 1/4: Node','STAGE 2/4: DPM','STAGE 3/4: Daml build','STAGE 4/4: Daml tests']) { + assert.match(nodeVerify, new RegExp(marker.replaceAll('/', '\\/'))); + } + assert.match(nodeVerify, /function trace\(message\)/); + assert.match(nodeVerify, /async function run\(label, command, args/); + assert.match(nodeVerify, /const child = spawn\(command, args/); + assert.match(nodeVerify, /child\.on\("close", \(code, signal\)/); + assert.match(nodeVerify, /verifier-trace\.log/); + assert.match(nodeVerify, /readdirSync\(resolve\(root, "tests"\)\)/); + assert.match(nodeVerify, /NODE_TEST_FILES/); + assert.match(nodeVerify, /await run\("STAGE 1\/4: Node", process\.execPath, \["--test", \.\.\.testFiles\]/); + assert.match(nodeVerify, /await run\("STAGE 2\/4: DPM", "dpm", \["version", "--active"\]/); + assert.match(nodeVerify, /await run\("STAGE 3\/4: Daml build", "dpm", \["build"\]/); + assert.match(nodeVerify, /await run\("STAGE 4\/4: Daml tests", "dpm", \["test"\]/); + assert.match(nodeVerify, /BUILD\/TEST VERIFICATION COMPLETE/); +}); + +test('real proof runner invokes the Node verifier directly', () => { + assert.match(runner, /node scripts\/verify-all\.mjs/); + assert.doesNotMatch(runner, /bash scripts\/verify-all\.sh/); +}); diff --git a/tests/sandbox-readiness.test.mjs b/tests/sandbox-readiness.test.mjs new file mode 100644 index 0000000..13ba697 --- /dev/null +++ b/tests/sandbox-readiness.test.mjs @@ -0,0 +1,23 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const script = await fs.readFile(new URL('../scripts/run-local-proof.sh', import.meta.url), 'utf8'); + +test('uses the DPM 3.5.12 canton-port-file readiness flag only', () => { + assert.match(script, /--canton-port-file\s+evidence\/canton-ports\.json/); + assert.doesNotMatch(script, /--port-file\b/); + assert.doesNotMatch(script, /--json-api-port-file\b/); +}); + +test('chooses a free loopback JSON API port per run', () => { + assert.match(script, /s\.bind\(\("127\.0\.0\.1",0\)\)/); + assert.match(script, /CANTON_JSON_API_URL="http:\/\/127\.0\.0\.1:\$CANTON_JSON_API_PORT"/); +}); + +test('waits for Canton ready signal before allocating parties', () => { + const waitIndex = script.indexOf('test -s evidence/canton-ports.json'); + const bootstrapIndex = script.indexOf('node scripts/bootstrap-local.mjs'); + assert.ok(waitIndex >= 0, 'must wait for canton-port-file'); + assert.ok(bootstrapIndex > waitIndex, 'party bootstrap must happen after ready signal'); +}); diff --git a/tests/security-regressions.test.mjs b/tests/security-regressions.test.mjs new file mode 100644 index 0000000..0f05efd --- /dev/null +++ b/tests/security-regressions.test.mjs @@ -0,0 +1,145 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { referencesIssue, normalizePullRequest, assertExpectedPullRequest, buildMergeEvidence, fetchVerifiedPullRequest, parseGitHubIssueUrl } from '../src/github-verifier.mjs'; +import { CantonApiError, expectLedgerFailure } from '../src/ledger-errors.mjs'; +import { CantonJsonApi } from '../src/canton-json-api.mjs'; +import { runtimeConfigFromEnv } from '../src/orchestrator.mjs'; + +const repository = 'Saidur-droid/CommitLedger'; +const head = 'a'.repeat(40); +const merge = 'b'.repeat(40); +const payload = { + number: 6, html_url: `https://github.com/${repository}/pull/6`, body: 'References #5.', + merged: true, merged_at: '2026-09-27T10:57:30Z', merge_commit_sha: merge, + user: {login: 'Saidur-droid'}, head: {sha: head}, base: {ref: 'main', repo: {full_name: repository}} +}; +for (const body of [ + '#5', 'Refs #5.', '(#5)', 'References Saidur-droid/CommitLedger#5', + `https://github.com/${repository}/issues/5`, `[issue](https://github.com/${repository}/issues/5)`, + 'Refs saidur-droid/commitledger#5' +]) test(`accepts explicit matching issue reference: ${body}`, () => { + assert.equal(referencesIssue(body, repository, 5), true); +}); +for (const body of [ + 'another/repository#5', 'https://github.com/another/repository/issues/5', + 'https://example.com/issues/5', 'https://example.com/#5', '#50', '#5foo', + '[#5](https://github.com/another/repository/issues/5)', '`#5`', '', + '\\#5', 'issue #500', 'https://github.com.evil.example/Saidur-droid/CommitLedger/issues/5', + 'evil/Saidur-droid/CommitLedger#5', 'http://github.com/Saidur-droid/CommitLedger/issues/5' +]) test(`rejects misleading issue reference: ${body}`, () => { + assert.equal(referencesIssue(body, repository, 5), false); +}); +test('rejects issue URLs with unsafe schemes or identity mismatch', () => { + for (const url of ['http://github.com/a/b/issues/5', 'https://user@github.com/a/b/issues/5', 'https://github.com/a/b/issues/9007199254740992']) { + assert.throws(() => parseGitHubIssueUrl(url)); + } +}); +test('merge commit SHA is required and included in the hash', () => { + const expected = { repository, prNumber: 6, issueNumber: 5 }; + for (const value of [null, '', 'abc', 'z'.repeat(40)]) { + assert.throws(() => assertExpectedPullRequest(normalizePullRequest({...payload, merge_commit_sha: value}), expected), /merge commit SHA/); + } + const a = buildMergeEvidence(assertExpectedPullRequest(normalizePullRequest(payload), expected)); + const b = buildMergeEvidence(assertExpectedPullRequest(normalizePullRequest({...payload, merge_commit_sha: 'c'.repeat(40)}), expected)); + assert.equal(a.mergeCommitSha, merge); + assert.notEqual(a.evidenceHash, b.evidenceHash); + assert.throws(() => assertExpectedPullRequest(normalizePullRequest(payload), {...expected, mergeCommitSha: 'd'.repeat(40)}), /merge commit SHA mismatch/); +}); +test('canonical merge commit and ancestry are verified server-side', async t => { + const urls = []; + t.mock.method(globalThis, 'fetch', async url => { + urls.push(url); + const value = url.includes('/pulls/') ? payload : url.includes('/commits/') ? {sha:merge} : {status:'ahead'}; + return Response.json(value); + }); + const evidence = await fetchVerifiedPullRequest({repository, prNumber:6, expected:{issueNumber:5, baseBranch:'main'}}); + assert.equal(evidence.mergeCommitSha, merge); + assert.equal(urls.length, 3); + assert.match(urls[1], /\/commits\/b{40}$/); + assert.match(urls[2], /\/compare\/b{40}\.\.\.main$/); +}); +test('a diverged base branch does not produce merge evidence', async t => { + t.mock.method(globalThis, 'fetch', async url => Response.json(url.includes('/pulls/') ? payload : url.includes('/commits/') ? {sha:merge} : {status:'diverged'})); + await assert.rejects(fetchVerifiedPullRequest({repository, prNumber:6, expected:{issueNumber:5}}), /not reachable/); +}); +test('a wrong canonical merge commit is rejected', async t => { + t.mock.method(globalThis, 'fetch', async url => Response.json(url.includes('/pulls/') ? payload : {sha:'c'.repeat(40)})); + await assert.rejects(fetchVerifiedPullRequest({repository, prNumber:6, expected:{issueNumber:5}}), /canonical merge commit SHA mismatch/); +}); +const expected = {codes:['DAML_AUTHORIZATION_ERROR']}; +for (const error of [ + new TypeError('fetch failed'), new Error('timeout'), + new CantonApiError(503, {code:'DAML_AUTHORIZATION_ERROR',cause:'server unavailable'}), + new CantonApiError(401, {code:'DAML_AUTHORIZATION_ERROR',cause:'expired JWT'}), + new CantonApiError(403, {code:'DAML_AUTHORIZATION_ERROR',cause:'token lacks rights'}), + new CantonApiError(429, {code:'DAML_AUTHORIZATION_ERROR',cause:'rate limit'}), + new CantonApiError(400, {code:'INVALID_ARGUMENT',cause:'wrong command schema'}), + new CantonApiError(400, {raw:'DAML_AUTHORIZATION_ERROR'}) +]) test(`does not treat infrastructure/configuration failure as authorization proof: ${error.message}`, async () => { + await assert.rejects(expectLedgerFailure('unauthorized', async () => {throw error;}, expected), /Unproven ledger rejection/); +}); +test('only an explicit ledger authorization error proves rejection', async () => { + const result = await expectLedgerFailure('unauthorized', async () => { + throw new CantonApiError(400, {code:'DAML_AUTHORIZATION_ERROR',cause:'Wrong controller'}); + }, expected); + assert.equal(result.rejected, true); + assert.equal(result.code, 'DAML_AUTHORIZATION_ERROR'); +}); +test('an unexpectedly successful negative check fails the run', async () => { + await assert.rejects(expectLedgerFailure('unauthorized', async () => ({}), expected), /did not occur/); +}); +test('wrong-issue rejection must name the issue mismatch, not another assertion', async () => { + await assert.rejects(expectLedgerFailure('wrong issue', async () => { + throw new CantonApiError(400, {code:'DAML_UNHANDLED_EXCEPTION',cause:'different failed assertion'}); + }, {codes:['DAML_UNHANDLED_EXCEPTION'],contains:'evidence issue number mismatch'}), /Unproven/); +}); +test('Canton client preserves structured error details', async t => { + t.mock.method(globalThis, 'fetch', async () => Response.json({code:'DAML_AUTHORIZATION_ERROR',cause:'wrong role'}, {status:400})); + const api = new CantonJsonApi({baseUrl:'http://localhost:3975',token:'unit-token'}); + await assert.rejects(api.request('/unit-test'), e => e instanceof CantonApiError && e.code === 'DAML_AUTHORIZATION_ERROR' && e.status === 400); +}); +test('Canton commands use v2 userId and synchronizerId names', async t => { + let body; + t.mock.method(globalThis, 'fetch', async (_url, options) => {body=JSON.parse(options.body);return Response.json({});}); + const client=new CantonJsonApi({baseUrl:'http://localhost:3975',token:'unit-token'}); client.packageIdSelectionPreference=['a'.repeat(64)]; + await client.submitAndWait({commands:[],actAs:[],commandId:'unit'}); + assert.ok('userId' in body && 'synchronizerId' in body); + assert.equal('applicationId' in body, false); + assert.equal('domainId' in body, false); + assert.deepEqual(body.packageIdSelectionPreference,['a'.repeat(64)]); +}); +test('insecure sandbox configuration cannot target a remote server', () => { + assert.throws(() => new CantonJsonApi({baseUrl:'https://remote.example',insecureLocal:true}), /loopback/); + assert.throws(() => runtimeConfigFromEnv({CANTON_JSON_API_URL:'https://remote.example',CANTON_INSECURE_LOCAL:'true'}), /loopback/); +}); +test('ambiguous active contracts cannot silently choose an old demo receipt', async t => { + const entry = {contractEntry:{JsActiveContract:{createdEvent:{contractId:'unit-cid',templateId:'a'.repeat(64)+':CommitLedger:Bounty',createArgument:{}}}}}; + t.mock.method(globalThis, 'fetch', async () => Response.json([entry,entry])); + const api=new CantonJsonApi({baseUrl:'http://localhost:3975',token:'unit-token'}); + await assert.rejects(api.findActiveContract({party:'unit-party',templateId:'#commit-ledger:CommitLedger:Bounty',activeAtOffset:1}), /Ambiguous/); +}); + + +test('active-contract lookup encodes activeAtOffset as a JSON int64', async t => { + let body; + t.mock.method(globalThis,'fetch',async (_url,options)=>{ body=JSON.parse(options.body); return Response.json([]); }); + const api=new CantonJsonApi({baseUrl:'http://localhost:3975',token:'unit-token'}); + await api.activeContracts({party:'unit-party',templateId:'pkg:CommitLedger:Bounty',activeAtOffset:42}); + assert.equal(body.activeAtOffset,42); +}); + +test('submit-and-wait normalizes nested Daml numeric values to JSON strings', async t => { + let body; + t.mock.method(globalThis, 'fetch', async (_url, options) => { body = JSON.parse(options.body); return Response.json({}); }); + const api = new CantonJsonApi({baseUrl:'http://localhost:3975',token:'unit-token'}); + await api.submitAndWait({ + commands:[{CreateCommand:{templateId:'#commit-ledger:CommitLedger:Bounty',createArguments:{issueNumber:5,rewardAmount:100.0,nested:{prNumber:6},items:[7]}}}], + actAs:['Maintainer::1'], + commandId:'unit-numeric-normalization' + }); + const args = body.commands[0].CreateCommand.createArguments; + assert.equal(args.issueNumber, '5'); + assert.equal(args.rewardAmount, '100'); + assert.equal(args.nested.prNumber, '6'); + assert.deepEqual(args.items, ['7']); +}); diff --git a/tests/server.test.mjs b/tests/server.test.mjs new file mode 100644 index 0000000..3b0cafb --- /dev/null +++ b/tests/server.test.mjs @@ -0,0 +1,60 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {request as httpRequest} from 'node:http'; +import {createAppServer} from '../src/server.mjs'; +async function withServer(t) { + const server=createAppServer({}); + await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); + t.after(()=>new Promise(resolve=>{server.close(resolve);server.closeAllConnections();})); + return `http://127.0.0.1:${server.address().port}`; +} +test('health never calls unconfigured Canton ready',async t=>{ + const base=await withServer(t);const value=await (await fetch(base+'/api/health')).json(); + assert.equal(value.lifecycleConfigured,false);assert.equal(value.ledgerReachable,false); +}); +test('verified proof endpoint fails closed when build artifacts are absent',async t=>{ + const base=await withServer(t);const response=await fetch(base+'/api/proof'); + assert.equal(response.status,503); +}); +test('unconfigured demo fails closed',async t=>{ + const base=await withServer(t);const response=await fetch(base+'/api/demo/run',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}); + assert.equal(response.status,503); +}); +test('arbitrary browser commands cannot use server-held ledger credentials',async t=>{ + const base=await withServer(t);const response=await fetch(base+'/api/canton/submit-command',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}); + assert.equal(response.status,403); +}); +test('cross-origin writes and simple-form content types are rejected',async t=>{ + const base=await withServer(t); + const a=await fetch(base+'/api/demo/run',{method:'POST',headers:{'Content-Type':'application/json',Origin:'https://evil.example'},body:'{}'}); + assert.equal(a.status,403); + const b=await fetch(base+'/api/demo/run',{method:'POST',headers:{'Content-Type':'text/plain'},body:'{}'}); + assert.equal(b.status,415); +}); + +test('configured public host accepts Render-style forwarded HTTPS origin',async t=>{ + const server=createAppServer({COMMITLEDGER_PUBLIC_HOST:'commitledger-proof-final.onrender.com'}); + await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); + t.after(()=>new Promise(resolve=>{server.close(resolve);server.closeAllConnections();})); + const port=server.address().port; + const result=await new Promise((resolve,reject)=>{ + const req=httpRequest({ + host:'127.0.0.1', + port, + path:'/api/demo/run', + method:'POST', + headers:{ + Host:'commitledger-proof-final.onrender.com', + 'X-Forwarded-Proto':'https', + Origin:'https://commitledger-proof-final.onrender.com', + 'Content-Type':'application/json' + } + },res=>{ + res.resume(); + res.on('end',()=>resolve(res.statusCode)); + }); + req.on('error',reject); + req.end('{}'); + }); + assert.equal(result,503); +}); diff --git a/web/app.js b/web/app.js index b009518..609f78e 100644 --- a/web/app.js +++ b/web/app.js @@ -1,156 +1,69 @@ -const bountyForm = document.querySelector("#bounty-form"); -const verifyForm = document.querySelector("#verify-form"); -const liveForm = document.querySelector("#live-form"); -const liveRun = document.querySelector("#live-run"); -const liveStatus = document.querySelector("#live-status"); -const timeline = document.querySelector("#timeline"); -const receiptCard = document.querySelector("#receipt-card"); -const issueStatus = document.querySelector("#issue-status"); -const githubStatus = document.querySelector("#github-status"); -const cantonBadge = document.querySelector("#canton-badge"); -const bountySummary = document.querySelector("#bounty-summary"); -const githubOutput = document.querySelector("#github-output"); - -let health = {}; - -async function request(path, options = {}) { - const response = await fetch(path, { - ...options, - headers: { "Content-Type": "application/json", ...(options.headers || {}) } +const $=selector=>document.querySelector(selector); +let ready=false,lastProof=null; +const escape=value=>String(value??'').replace(/[&<>"']/g,char=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])); +async function request(path,body) { + const response=await fetch(path,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:{}); + const value=await response.json(); + if(!response.ok) throw new Error(value.error||'Request failed'); + return value; +} +const names={BOUNTY_ON_LEDGER:'Bounty created',CLAIM_REQUESTED:'Claim requested',CLAIMED:'Claim accepted',PR_SUBMITTED:'Pull request submitted',VERIFIED:'Merge verified',SETTLED:'Settlement recorded'}; +function render(proof) { + $('#timeline').innerHTML=proof.steps.map((step,i)=>`
${i+1}. ${escape(names[step.name]||step.name)}Ledger offset ${escape(step.completionOffset)}
Inspect full ledger referencesContract ID${escape(step.contractId)}Update ID${escape(step.updateId)}Template${escape(step.templateId)}
`).join(''); + const r=proof.settlementReceipt; + $('#receipt-card').classList.remove('hidden'); + $('#receipt-card').innerHTML=`

SETTLEMENT RECEIPT / DEMO VALUE

${escape(r.rewardAmount)} ${escape(r.rewardUnit)}

Repository and issue${escape(r.repository)} #${escape(r.issueNumber)}
Pull request#${escape(r.pullRequest.prNumber)}
Source commit${escape(proof.sourceCommit)}
Merge commit SHA${escape(r.mergeCommitSha)}
Evidence hash${escape(r.evidenceHash)}
Package ID${escape(proof.packageId)}
${proof.negativeChecks.map(c=>`
${escape(c.name)}${escape(c.code)}
Inspect rejection response
${escape(JSON.stringify(c.response,null,2))}
`).join('')}

${escape(proof.environment)}. This receipt does not represent a transfer of real money.

`; + $('#download-proof').addEventListener('click',()=>{ + const url=URL.createObjectURL(new Blob([JSON.stringify(lastProof,null,2)+'\n'],{type:'application/json'})); + const a=document.createElement('a');a.href=url;a.download=`commitledger-proof-${lastProof.runId}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000); }); - const data = await response.json(); - if (!response.ok) throw new Error(data.error || "Request failed"); - return data; } - -async function checkHealth() { +async function loadProof() { try { - health = await request("/api/health"); - cantonBadge.textContent = health.lifecycleConfigured ? "Canton lifecycle ready" : "Canton setup pending"; - liveStatus.textContent = health.lifecycleConfigured ? "Ready for real ledger run" : "Runtime not configured"; - liveRun.disabled = !health.lifecycleConfigured; - } catch { - cantonBadge.textContent = "Local app offline"; - liveStatus.textContent = "Offline"; - liveRun.disabled = true; + const {proof,verification}=await request('/api/proof'); + lastProof=proof; + render(proof); + $('#canton-badge').textContent='Verified Canton proof'; + $('#live-status').textContent='Verified deployment proof loaded'; + $('#configuration-note').textContent=`Read-only proof for source ${proof.sourceCommit}. Node/Daml verification commit: ${verification.commit}. The build-time Canton sandbox is intentionally not persistent.`; + } catch(error) { + $('#canton-badge').textContent='Proof unavailable'; + $('#live-status').textContent='Verified proof unavailable'; + $('#configuration-note').textContent=error.message; } } - -liveForm.addEventListener("submit", async (event) => { - event.preventDefault(); - if (!health.lifecycleConfigured) return; - liveRun.disabled = true; - liveStatus.textContent = "Submitting to Canton…"; - timeline.innerHTML = '
Executing real ledger transitions. No mocked steps are shown.
'; - receiptCard.classList.add("hidden"); +async function health() { try { - const input = Object.fromEntries(new FormData(liveForm)); - input.prNumber = Number(input.prNumber); - input.rewardAmount = Number(input.rewardAmount); - const data = await request("/api/demo/run", { - method: "POST", - body: JSON.stringify(input) - }); - liveStatus.textContent = "Settlement receipt verified"; - renderTimeline(data.proof.steps); - renderReceipt(data.proof); - } catch (error) { - liveStatus.textContent = "Ledger run failed"; - timeline.innerHTML = `
${escapeHtml(error.message)}
`; - } finally { - liveRun.disabled = !health.lifecycleConfigured; + const h=await request('/api/health');ready=h.lifecycleConfigured && h.ledgerReachable; + if(!lastProof) { + $('#canton-badge').textContent=h.verifiedProofAvailable?'Verified proof available':ready?'Canton connected':'Canton runtime offline'; + $('#live-status').textContent=ready?'Ready for another real ledger run':h.verifiedProofAvailable?'Deployment proof available':'Runtime not configured'; + $('#configuration-note').textContent=h.configurationError||(!h.ledgerReachable?'The public service does not keep the build-time sandbox alive.':'Connected. No settlement has been claimed until a receipt is returned.'); + } + } catch(error) { + ready=false; + if(!lastProof) {$('#canton-badge').textContent='App offline';$('#configuration-note').textContent=error.message;} } + $('#live-run').disabled=!ready; +} +$('#refresh-health').addEventListener('click',async()=>{await health();await loadProof();}); +$('#live-form').addEventListener('submit',async event=>{ + event.preventDefault();if(!ready)return; + lastProof=null;$('#live-run').disabled=true;$('#receipt-card').classList.add('hidden'); + $('#live-status').textContent='Executing real ledger commands';$('#timeline').innerHTML='
Waiting for actual Canton responses. No simulated success.
'; + try {const data=await request('/api/demo/run',Object.fromEntries(new FormData(event.target)));lastProof=data.proof;render(lastProof);$('#live-status').textContent='Settlement receipt returned';} + catch(error){$('#live-status').textContent='Ledger run failed';$('#timeline').innerHTML=`
${escape(error.message)}

No successful proof bundle was produced.

`;} + finally{$('#live-run').disabled=!ready;} }); - -bountyForm.addEventListener("submit", async (event) => { - event.preventDefault(); - issueStatus.textContent = "Verifying"; - bountySummary.className = "summary muted"; - bountySummary.textContent = "Reading canonical GitHub issue…"; - try { - const input = Object.fromEntries(new FormData(bountyForm)); - input.rewardAmount = Number(input.rewardAmount); - const data = await request("/api/bounty/prepare", { - method: "POST", - body: JSON.stringify(input) - }); - issueStatus.textContent = "Issue verified"; - bountySummary.className = "summary"; - bountySummary.innerHTML = ` - ${escapeHtml(data.bounty.title)} - ${escapeHtml(data.bounty.repository)} #${data.bounty.issueNumber} - ${data.bounty.rewardAmount} ${escapeHtml(data.bounty.rewardUnit)} · demo/test value - `; - } catch (error) { - issueStatus.textContent = "Rejected"; - bountySummary.textContent = error.message; - } +$('#bounty-form').addEventListener('submit',async event=>{ + event.preventDefault();$('#issue-status').textContent='Verifying'; + try {const {bounty}=await request('/api/bounty/prepare',Object.fromEntries(new FormData(event.target)));$('#issue-status').textContent='Issue verified';$('#bounty-summary').textContent=`${bounty.title} / ${bounty.repository} #${bounty.issueNumber} / ${bounty.rewardAmount} ${bounty.rewardUnit}`;} + catch(error){$('#issue-status').textContent='Verification failed';$('#bounty-summary').textContent=error.message;} }); - -verifyForm.addEventListener("submit", async (event) => { - event.preventDefault(); - githubStatus.textContent = "Verifying"; - githubOutput.textContent = "Reading canonical GitHub pull request…"; - const values = Object.fromEntries(new FormData(verifyForm)); - values.issueNumber = Number(values.issueNumber); - values.prNumber = Number(values.prNumber); - try { - const data = await request("/api/github/verify", { - method: "POST", - body: JSON.stringify(values) - }); - githubStatus.textContent = "Merge verified"; - githubOutput.textContent = JSON.stringify(data.evidence, null, 2); - } catch (error) { - githubStatus.textContent = "Rejected"; - githubOutput.textContent = error.message; - } +$('#verify-form').addEventListener('submit',async event=>{ + event.preventDefault();$('#github-status').textContent='Verifying'; + try {const {evidence}=await request('/api/github/verify',Object.fromEntries(new FormData(event.target)));$('#github-status').textContent='Merge verified';$('#github-output').textContent=JSON.stringify(evidence,null,2);} + catch(error){$('#github-status').textContent='Verification failed';$('#github-output').textContent=error.message;} }); - -function renderTimeline(steps) { - timeline.innerHTML = steps.map((step, index) => ` -
-
${String(index + 1).padStart(2, "0")}
-
- ${escapeHtml(step.name)} - contract · ${escapeHtml(shortId(step.contractId))} - update · ${escapeHtml(shortId(step.updateId))} · offset ${step.completionOffset} -
-
- `).join(""); -} - -function renderReceipt(proof) { - const receipt = proof.settlementReceipt || {}; - receiptCard.classList.remove("hidden"); - receiptCard.innerHTML = ` -

SETTLEMENT RECEIPT

-

${escapeHtml(receipt.bountyId || "Settled bounty")}

-
-
Repository${escapeHtml(receipt.repository || "")}
-
Pull request#${receipt.pullRequest?.prNumber ?? ""}
-
Reward${escapeHtml(receipt.rewardAmount || "")} ${escapeHtml(receipt.rewardUnit || "")}
-
Evidence${escapeHtml(shortId(receipt.evidenceHash || ""))}
-
Settlement ref${escapeHtml(receipt.settlementRef || "")}
-
Canton proof${proof.steps.length} committed transitions
-
Negative checks${proof.negativeChecks?.filter(check => check.rejected).length || 0} enforced rejections
-
- `; -} - -function shortId(value) { - const text = String(value || ""); - return text.length > 22 ? `${text.slice(0, 10)}…${text.slice(-8)}` : text; -} - -function escapeHtml(value) { - return String(value).replace(/[&<>"']/g, char => ({ - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'" - })[char]); -} - -checkHealth(); +await health(); +await loadProof(); diff --git a/web/index.html b/web/index.html index 33415c1..8cc36ec 100644 --- a/web/index.html +++ b/web/index.html @@ -1,136 +1,39 @@ - - - - CommitLedger — Verified work, settled on Canton - + + +CommitLedger - Verified work, settled on Canton - - -
-
-

GITHUB EVIDENCE × DAML AUTHORIZATION × CANTON SETTLEMENT

-

Work is not settled until the proof is verifiable.

-

CommitLedger turns a real GitHub issue into a role-authorized bounty lifecycle and records the final evidence-bound receipt on Canton.

-
- 1 · Issue→2 · Bounty→3 · Claim→4 · PR→5 · Verify→6 · Settle -
-
- -
-
-
-

LIVE CANTON PROOF

-

Run the complete six-step lifecycle

-
- Checking runtime -
-

This button performs real JSON Ledger API submissions. It is disabled until Canton package, three distinct parties, and authorized token(s) are configured.

-
- -
- - - -
- -
-
-
No Canton lifecycle has been executed in this session.
-
- -
- -
-
-
-
-

CANONICAL ISSUE

-

Verify a real GitHub work source

-
- Waiting -
-
- -
- - -
- -
-
GitHub is queried server-side. The browser cannot invent the repository, issue title, or issue state.
-
- - -
- -
-
-
-
-

CANONICAL MERGE

-

Verify the exact merged pull request

-
- Waiting -
-
- -
- - - -
- - -
-
No merge evidence loaded.
-
- - -
- -
-

JUDGE PROOF

-

Every important claim leaves an inspectable artifact.

-
-
01Real issue

Canonical GitHub provider state.

-
02Contract IDs

One active contract at every stage.

-
03Update IDs

Every Canton submission is traceable.

-
04Evidence hash

Exact merged PR bound to settlement.

-
05Receipt

Final auditable Canton contract.

-
-
-
- -
CommitLedger · independent HackCanton competition build · DEMO_CREDIT is non-production test value
- - - + +
+

GITHUB EVIDENCE / DAML AUTHORIZATION / CANTON SETTLEMENT

Work is not settled until the proof is verifiable.

From merged code to inspectable settlement: canonical GitHub evidence, explicit multi-party authorization, and a Canton receipt in one judgeable workflow.

Issue→Bounty→Claim→Pull request→Verify→Settle→Receipt
+ +
+
SourceGitHub #69 → PR #73Open issue + canonical merged pull request
+
Authorization3 explicit rolesMaintainer · Contributor · Verifier
+
SettlementDEMO_CREDITNon-production test value
+
RuntimeVERIFIEDReal Canton proof captured during deploy
+
+ +
+

COMPETITION READINESS

Technical proof is green; external gates remain fail-closed

BLOCKED · external gates pending
+

The runtime proof is verified independently. Repository publication, final video, Mana/activity, journal and project-profile requirements remain blocked until they are actually verified.

+
PASSNode verification
PASSDaml + real Canton proof
BLOCKEDMana / activity / journal
BLOCKEDFinal public submission materials
+
+ +
+

VERIFIED CANTON PROOF

Inspect the deployed six-step lifecycle

Loading proof artifact
+

A fresh Canton sandbox ran during this deployment. This page exposes the resulting read-only proof bundle. The build-time sandbox is intentionally ephemeral; no sample transaction IDs or simulated settlement success are shown. DEMO_CREDIT is not real money.

+
+

Loading deployment evidence…

+
Loading verified deployment proof…
+
+ +

CANONICAL ISSUE

Verify the work source

Waiting
Issue identity and state are fetched server-side from GitHub.
+

CANONICAL MERGE

Verify the merged pull request

Waiting
No merge evidence loaded.
+ +

OFFICIAL JUDGING CRITERIA

Six questions, six visible answers

Value / ProblemGitHub proves code events; Canton enforces who may verify and settle.
ICP / AudienceOpen-source maintainers, foundations and contributor bounty programs.
Metrics / ValidationReal GitHub fixture, deterministic tests and a source-bound Canton proof.
GTM MaterialsSingle-repo pilot → foundation/ecosystem bounty program → settlement adapter.
MVP MaterialsGitHub verification + Daml authorization + Canton receipt + attack rejection.
Pitch MaterialsProblem → proof → rejected attacks → receipt → why Canton → pilot path.
+
CommitLedger is independent. DEMO_CREDIT is non-production test value. No mainnet, real-money or traction claim is made.
+ diff --git a/web/styles.css b/web/styles.css index 47c8935..d14ae22 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1 +1,3 @@ -:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eef4f5;background:#061014;line-height:1.5}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 18% -8%,#153b43 0,transparent 32%),linear-gradient(#071216,#061014);color:#eef4f5}.nav{height:76px;display:flex;align-items:center;justify-content:space-between;max-width:1200px;margin:auto;padding:0 24px;border-bottom:1px solid #1d3339}.brand{font-size:21px;font-weight:800;color:#fff;text-decoration:none;letter-spacing:-.035em}.subbrand{font-size:12px;color:#6f8d94;margin-left:12px}.nav-right{display:flex;gap:8px}.badge,.status{display:inline-flex;align-items:center;font-size:11px;border:1px solid #315058;border-radius:999px;padding:6px 10px;color:#b9ced3;background:#0b181d;white-space:nowrap}main{max-width:1200px;margin:auto;padding:52px 24px 84px}.hero{max-width:1050px;padding:42px 0 48px}.eyebrow,.kicker{font-size:11px;letter-spacing:.17em;color:#82adb6;font-weight:800;margin:0 0 12px}.hero h1{font-size:clamp(44px,7.2vw,86px);line-height:.96;letter-spacing:-.06em;margin:0;max-width:1080px}.lead{font-size:20px;color:#9eb5ba;max-width:820px;margin:24px 0}.flow{display:flex;flex-wrap:wrap;gap:9px;align-items:center;margin-top:28px}.flow span{padding:8px 11px;background:#0b171c;border:1px solid #254149;border-radius:9px;color:#a9bec3;font-size:13px}.flow b{color:#55767e}.workspace{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(280px,.65fr);gap:18px;margin-bottom:18px}.panel{border:1px solid #203940;background:rgba(8,20,25,.88);border-radius:18px;padding:24px;box-shadow:0 22px 70px rgba(0,0,0,.2)}.live-panel{margin-bottom:18px;border-color:#365f68;background:linear-gradient(135deg,rgba(11,31,36,.96),rgba(7,20,24,.9))}.primary{min-height:100%}.proof-card{background:rgba(8,17,21,.7)}.panel-head{display:flex;justify-content:space-between;gap:18px;align-items:flex-start}.panel h2,.proof h2{font-size:26px;letter-spacing:-.04em;margin:0 0 18px}.metric{border-top:1px solid #213840;padding:14px 0;display:grid;gap:2px}.metric strong{font-size:14px}.metric span,.muted{color:#829ca2;font-size:14px}label{display:grid;gap:7px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#8da7ad;margin:12px 0}input{width:100%;border:1px solid #29474f;border-radius:10px;background:#061014;color:#eef5f6;padding:12px 13px;font:inherit;text-transform:none;letter-spacing:0;outline:none}input:focus{border-color:#78aab5;box-shadow:0 0 0 3px rgba(120,170,181,.12)}input:disabled{opacity:.65}.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}.row.three{grid-template-columns:.7fr 1.2fr .7fr}button{border:0;border-radius:10px;background:#e6f0f1;color:#061014;padding:12px 16px;font-weight:850;cursor:pointer;margin-top:6px}button:hover:not(:disabled){transform:translateY(-1px)}button:disabled{cursor:not-allowed;opacity:.45}.summary{display:grid;gap:4px;margin-top:18px;padding:15px;border:1px solid #2c4b53;border-radius:12px;background:#08171b}.summary strong{font-size:16px}.summary span{color:#91aab0;font-size:13px}.output{min-height:150px;max-height:320px;overflow:auto;background:#040b0d;border:1px solid #1a3036;border-radius:10px;padding:14px;color:#9fc0c6;font-size:12px;white-space:pre-wrap}.checks{padding-left:18px;color:#9fb5ba}.checks li{margin:14px 0}.timeline{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:22px}.timeline-empty,.timeline-error{grid-column:1/-1;padding:18px;border:1px dashed #315059;border-radius:12px;color:#829ba1}.timeline-error{border-color:#704047;color:#d5a7ac}.timeline-step{display:flex;gap:10px;border:1px solid #29474e;border-radius:12px;padding:14px;background:#071519;min-width:0}.step-index{font-size:12px;color:#6e9da7;font-weight:800}.timeline-step strong,.timeline-step span{display:block}.timeline-step strong{font-size:13px;margin-bottom:5px}.timeline-step span{font-size:11px;color:#7f9ca3;overflow:hidden;text-overflow:ellipsis}.receipt{margin-top:16px;padding:20px;border:1px solid #47717a;border-radius:14px;background:rgba(15,38,44,.72)}.receipt.hidden{display:none}.receipt h3{margin:0 0 15px;font-size:24px;letter-spacing:-.03em}.receipt-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.receipt-grid div{border-top:1px solid #35565e;padding-top:10px;min-width:0}.receipt-grid span,.receipt-grid strong{display:block}.receipt-grid span{font-size:10px;color:#6f9098;text-transform:uppercase;letter-spacing:.08em}.receipt-grid strong{font-size:12px;margin-top:4px;overflow:hidden;text-overflow:ellipsis}.proof{padding-top:68px}.proof-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}.proof-grid>div{border-top:1px solid #315159;padding:17px 4px}.proof-grid strong{display:block;font-size:25px;color:#74a7b1}.proof-grid span{display:block;color:#f0f5f6;font-weight:750;margin:8px 0}.proof-grid p{font-size:13px;color:#7d979d;margin:0}footer{max-width:1200px;margin:auto;padding:24px;border-top:1px solid #1d3339;color:#68858c;font-size:12px}@media(max-width:900px){.workspace,.proof-grid,.timeline,.receipt-grid{grid-template-columns:1fr}.row,.row.three{grid-template-columns:1fr}.hero{padding-top:18px}.panel-head{display:block}.status{margin-bottom:10px}.subbrand{display:none}.nav-right .badge:first-child{display:none}} \ No newline at end of file +:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eef4f5;background:#061014;line-height:1.5}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 18% -8%,#153b43 0,transparent 32%),linear-gradient(#071216,#061014)}.nav{min-height:76px;display:flex;align-items:center;justify-content:space-between;max-width:1200px;margin:auto;padding:16px 24px;border-bottom:1px solid #1d3339;gap:12px}.brand{font-size:21px;font-weight:800;color:#fff;text-decoration:none;letter-spacing:-.035em}.nav-right{display:flex;gap:8px}.badge,.status{display:inline-flex;align-items:center;font-size:11px;border:1px solid #315058;border-radius:999px;padding:6px 10px;color:#b9ced3;background:#0b181d}main{max-width:1200px;margin:auto;padding:32px 24px 64px}.hero{max-width:1050px;padding:42px 0 48px}.eyebrow,.kicker{font-size:11px;letter-spacing:.15em;color:#91bac3;font-weight:800;margin:0 0 12px}.hero h1{font-size:clamp(40px,6.5vw,78px);line-height:1.03;letter-spacing:-.055em;margin:0}.lead{font-size:20px;color:#a9c0c6;max-width:820px;margin:24px 0}.flow{display:flex;flex-wrap:wrap;gap:9px;align-items:center;margin-top:28px}.flow span{padding:8px 11px;background:#0b171c;border:1px solid #254149;border-radius:9px;color:#a9bec3;font-size:13px}.flow b{color:#55767e}.workspace{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(280px,.65fr);gap:18px;margin-bottom:18px}.panel{border:1px solid #203940;background:rgba(8,20,25,.88);border-radius:18px;padding:24px;min-width:0}.live-panel{margin-bottom:18px;border-color:#365f68;background:linear-gradient(135deg,rgba(11,31,36,.96),rgba(7,20,24,.9))}.panel-head{display:flex;justify-content:space-between;gap:18px;align-items:flex-start}.panel h2{font-size:26px;letter-spacing:-.03em;margin:0 0 18px}.metric{border-top:1px solid #213840;padding:14px 0;display:grid;gap:3px}.metric strong{font-size:14px}.metric span,.muted{color:#a0b6bc;font-size:14px}label{display:grid;gap:7px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#a7bfc5;margin:12px 0;min-width:0}input{width:100%;min-width:0;border:1px solid #29474f;border-radius:10px;background:#061014;color:#eef5f6;padding:12px 13px;font:inherit;text-transform:none;letter-spacing:0}input:focus-visible,button:focus-visible{outline:2px solid #a3d6df;outline-offset:3px}input:disabled{opacity:.75}.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}.row.three{grid-template-columns:.7fr 1.2fr .7fr}button{border:0;border-radius:10px;background:#e6f0f1;color:#061014;padding:12px 16px;font-weight:800;cursor:pointer;margin:6px 6px 0 0}button:disabled{cursor:not-allowed;opacity:.45}.secondary{background:#193740;color:#d8e9ed;border:1px solid #365963}.summary{display:grid;gap:4px;margin-top:18px;padding:15px;border:1px solid #2c4b53;border-radius:12px;background:#08171b}.output{min-height:150px;max-height:360px;overflow:auto;background:#040b0d;border:1px solid #1a3036;border-radius:10px;padding:14px;color:#b2d1d7;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere}.timeline{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:22px}.timeline-empty,.timeline-error{grid-column:1/-1;padding:18px;border:1px dashed #315059;border-radius:12px;color:#a1b9bf}.timeline-error{border-color:#704047;color:#efbec2}.timeline-step{border:1px solid #29474e;border-radius:12px;padding:14px;background:#071519;min-width:0}.timeline-step strong,.timeline-step span{display:block}.timeline-step strong{font-size:14px;margin-bottom:5px}.timeline-step span{font-size:12px;color:#a1bac1}.timeline-step code,.receipt code{display:block;overflow-wrap:anywhere;font-size:11px;margin:5px 0;color:#c1dce2}details{margin-top:8px}summary{cursor:pointer;font-size:12px}.receipt{margin-top:16px;padding:20px;border:1px solid #47717a;border-radius:14px;background:rgba(15,38,44,.72);overflow-wrap:anywhere}.hidden{display:none}.receipt h3{margin:0 0 15px;font-size:22px}.receipt-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.receipt-grid span,.receipt-grid strong{display:block}.receipt-grid span{font-size:11px;color:#a2bdc5}.receipt-grid strong{font-size:13px;margin-top:4px}.negative{border-top:1px solid #35565e;margin-top:12px;padding-top:12px}footer{max-width:1200px;margin:auto;padding:24px;border-top:1px solid #1d3339;color:#98b0b7;font-size:12px}@media(max-width:900px){.workspace,.timeline,.receipt-grid,.row,.row.three{grid-template-columns:1fr}.hero{padding-top:18px}.panel-head{display:block}.status{margin-bottom:10px}.panel{padding:20px}main{padding:20px 16px 40px}.nav{padding:14px 16px}.nav-right .badge:last-child{display:none}.eyebrow{line-height:1.8}.lead{font-size:17px}} +.readiness-panel,.criteria-panel{margin-bottom:18px}.status.blocked{border-color:#7a6540;color:#e4bc68}.readiness-grid,.criteria-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:18px}.readiness-grid>div,.criteria-grid article{border:1px solid #29474f;border-radius:12px;padding:14px;background:#071519;min-width:0}.readiness-grid strong,.readiness-grid span,.criteria-grid strong,.criteria-grid span{display:block}.readiness-grid strong,.criteria-grid strong{font-size:13px;margin-bottom:5px}.readiness-grid span,.criteria-grid span{font-size:12px;color:#a1bac1}.criteria-grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:900px){.readiness-grid,.criteria-grid{grid-template-columns:1fr}} +.evidence-rail{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0 0 18px}.evidence-rail article{border:1px solid #29474f;border-radius:14px;padding:16px;background:linear-gradient(180deg,rgba(11,24,29,.94),rgba(7,21,25,.9));min-width:0}.evidence-rail span,.evidence-rail strong,.evidence-rail small{display:block}.evidence-rail span{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:#91bac3;font-weight:800}.evidence-rail strong{font-size:18px;letter-spacing:-.02em;margin:6px 0 3px;color:#eef4f5}.evidence-rail small{font-size:12px;color:#a0b6bc}.evidence-rail .runtime-verified{border-color:#3f7161}.evidence-rail .runtime-verified strong{color:#9fe0c4}@media(max-width:900px){.evidence-rail{grid-template-columns:1fr 1fr}}@media(max-width:560px){.evidence-rail{grid-template-columns:1fr}}