@finchip.ai/foga 0.0.0-stage → 0.6.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +216 -2
  4. package/artifacts/bundle.json +74250 -0
  5. package/artifacts/bundle.sha256 +1 -0
  6. package/bin/foga.js +14 -0
  7. package/build/known-interface-ids.json +18 -0
  8. package/build/profiles.json +120 -0
  9. package/dist/build/bundle.js +417 -0
  10. package/dist/build/vendor.js +74 -0
  11. package/dist/bundle/load.js +24 -0
  12. package/dist/bundle/types.js +1 -0
  13. package/dist/cli-cell.js +395 -0
  14. package/dist/cli.js +505 -0
  15. package/dist/core/acdf.js +162 -0
  16. package/dist/core/adrf.js +273 -0
  17. package/dist/core/aid.js +83 -0
  18. package/dist/core/ard.js +146 -0
  19. package/dist/core/asf.js +157 -0
  20. package/dist/core/canonical.js +79 -0
  21. package/dist/core/erc8004.js +52 -0
  22. package/dist/core/kya.js +225 -0
  23. package/dist/core/measure.js +96 -0
  24. package/dist/core/pack/cbor.js +130 -0
  25. package/dist/core/pack/skillpack.js +142 -0
  26. package/dist/core/skills.js +40 -0
  27. package/dist/core/swarm.js +206 -0
  28. package/dist/core/tenders.js +121 -0
  29. package/dist/core/units.js +122 -0
  30. package/dist/core/world.js +104 -0
  31. package/dist/demo/demander.js +76 -0
  32. package/dist/demo/evidence.js +1 -0
  33. package/dist/demo/loop-d.js +449 -0
  34. package/dist/doctor/checks.js +51 -0
  35. package/dist/doctor/doctor.js +385 -0
  36. package/dist/doctor/loop-d.js +103 -0
  37. package/dist/doctor/probe.js +97 -0
  38. package/dist/doctor/semantic.js +83 -0
  39. package/dist/genesis/extract.js +64 -0
  40. package/dist/genesis/funding.js +126 -0
  41. package/dist/genesis/operator-scope.js +90 -0
  42. package/dist/genesis/preflight.js +54 -0
  43. package/dist/genesis/probe.js +164 -0
  44. package/dist/genesis/roles.js +96 -0
  45. package/dist/genesis/run.js +71 -0
  46. package/dist/genesis/scenario.js +765 -0
  47. package/dist/genesis/skill-runner.js +103 -0
  48. package/dist/genesis/skill-worker.js +41 -0
  49. package/dist/genesis/t1-zk.js +62 -0
  50. package/dist/genesis/t3-zk.js +105 -0
  51. package/dist/genesis/t4.js +259 -0
  52. package/dist/genesis/t5.js +168 -0
  53. package/dist/genesis/t6.js +222 -0
  54. package/dist/genesis/t7.js +198 -0
  55. package/dist/genesis/t8.js +189 -0
  56. package/dist/genesis/t9.js +188 -0
  57. package/dist/genesis/zk.js +128 -0
  58. package/dist/http/server.js +276 -0
  59. package/dist/kernel/cell.js +104 -0
  60. package/dist/kernel/errors.js +78 -0
  61. package/dist/kernel/kernel.js +251 -0
  62. package/dist/kernel/provider.js +125 -0
  63. package/dist/kernel/store.js +470 -0
  64. package/dist/kernel/tx.js +385 -0
  65. package/dist/mcp/server.js +170 -0
  66. package/dist/node/config.js +195 -0
  67. package/dist/node/init.js +200 -0
  68. package/dist/node/keys.js +95 -0
  69. package/dist/node/migrate.js +173 -0
  70. package/dist/node/model/anthropic.js +97 -0
  71. package/dist/node/model/stub.js +129 -0
  72. package/dist/node/model/types.js +9 -0
  73. package/dist/node/node.js +198 -0
  74. package/dist/node/policy.js +95 -0
  75. package/dist/node/runner.js +256 -0
  76. package/dist/node/watchers.js +88 -0
  77. package/dist/tools/admin.js +184 -0
  78. package/dist/tools/catalog.js +261 -0
  79. package/dist/tools/common.js +143 -0
  80. package/dist/tools/identity.js +107 -0
  81. package/dist/tools/index.js +14 -0
  82. package/dist/tools/read.js +471 -0
  83. package/dist/tools/tenders.js +400 -0
  84. package/dist/tools/types.js +4 -0
  85. package/dist/util/bytecode.js +123 -0
  86. package/dist/util/fs.js +58 -0
  87. package/dist/util/hash.js +4 -0
  88. package/dist/util/log.js +11 -0
  89. package/dist/util/version.js +12 -0
  90. package/dist/world/deploy.js +277 -0
  91. package/dist/world/plan.js +93 -0
  92. package/dist/world/types.js +1 -0
  93. package/docs/build.md +45 -0
  94. package/docs/cell-account.md +58 -0
  95. package/docs/compatibility-matrix.md +38 -0
  96. package/docs/design-memo.md +113 -0
  97. package/docs/doctor.md +53 -0
  98. package/docs/examples/README.md +11 -0
  99. package/docs/examples/claude-code-mcp.json +13 -0
  100. package/docs/examples/claude_desktop_config.json +12 -0
  101. package/docs/examples/cursor-mcp.json +11 -0
  102. package/docs/genesis.md +312 -0
  103. package/docs/kernel.md +101 -0
  104. package/docs/loop-d.md +90 -0
  105. package/docs/mcp.md +105 -0
  106. package/docs/node.md +66 -0
  107. package/docs/version-lock.md +67 -0
  108. package/docs/world.md +50 -0
  109. package/docs/zk.md +48 -0
  110. package/package.json +82 -4
  111. package/prover/README.md +26 -0
  112. package/prover/build-predicate.mjs +77 -0
  113. package/prover/package-lock.json +1837 -0
  114. package/prover/package.json +15 -0
  115. package/prover/run.cjs +199 -0
  116. package/scenarios/fixtures/sample-filing.txt +16 -0
  117. package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
  118. package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
  119. package/scenarios/skills/finch-extract-verify/fields.json +56 -0
  120. package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
  121. package/scenarios/skills/foga-probe/LICENSE +2 -0
  122. package/scenarios/skills/foga-probe/SKILL.md +35 -0
  123. package/scenarios/skills/foga-probe/manifest.json +16 -0
  124. package/scenarios/skills/foga-probe/probe.schema.json +40 -0
  125. package/server.json +79 -0
  126. package/vendor/LOCK.json +107 -0
  127. package/vendor/README.md +13 -0
  128. package/worlds/local.json +34 -0
  129. package/worlds/sepolia.json +314 -0
  130. package/zk/build/verification_key.json +174 -0
  131. package/zk/circuits/kya_predicate_count_v1.circom +231 -0
@@ -0,0 +1,168 @@
1
+ import { join } from "node:path";
2
+ import { getAddress, parseEther } from "ethers";
3
+ import { TenderAdapterRef } from "../core/acdf.js";
4
+ import { dataUri, keccakJcs, sha256Hex } from "../core/canonical.js";
5
+ import { DecisionPoint, SwarmAccountRef, SwarmDecisionAdapterRef, SwarmTenderAdapterRef, packHashes } from "../core/swarm.js";
6
+ import { nativeTerms } from "../core/tenders.js";
7
+ import { runExtractIsolated } from "./skill-runner.js";
8
+ /**
9
+ * T5 — governance: collective acceptance, collective labour, collective decisions.
10
+ * (1) the swarm account submits the finch-extract-verify report to the judged tender of T4;
11
+ * (2) anyone opens the council's case; two judges approve; the round settles, the appeal window passes, the
12
+ * decision is Final; the adapter enacts acceptFulfillment and the swarm account is paid;
13
+ * (3) the workers amend the charter through the decision adapter (2 of 2);
14
+ * (4) a second judged tender ends without a decision: the adapter enacts nothing and the tender's own default
15
+ * path (claimUnjudged) pays the fulfiller after the judgment window;
16
+ * (5) the swarm tender adapter concludes the swarm ACHIEVED.
17
+ */
18
+ export async function t5(g) {
19
+ const k = g.kernel;
20
+ const W = "W";
21
+ const A = g.cell("A");
22
+ const B = g.cell("B");
23
+ const C = g.cell("C");
24
+ const chainId = g.chainId;
25
+ const timing = g.timing;
26
+ const tokenId = BigInt(k.requireFact("T4.tokenId"));
27
+ const swarmId = k.requireFact("T4.swarmId");
28
+ const extractSkill = k.requireFact("T2.skill.finch-extract-verify");
29
+ const judges = ["J1", "J2", "J3"].map((j) => k.address(j));
30
+ const adapter = new TenderAdapterRef(g.world, k.requireFact("T0.acdf.tenderAdapter"));
31
+ const account = new SwarmAccountRef(g.world, k.requireFact("T4.swarmAccount"));
32
+ const decision = new SwarmDecisionAdapterRef(g.world, k.requireFact("T4.decisionAdapter"));
33
+ const swarmTender = new SwarmTenderAdapterRef(g.world, g.world.address("8447/SwarmTenderAdapter"));
34
+ const effects = await adapter.effects();
35
+ // the judges are nobody's party: not the demander, not a member, not the swarm account, not an operator
36
+ const parties = new Set([k.address(W), A.address, B.address, C.address, account.address, A.operatorAddress, B.operatorAddress, C.operatorAddress].map((a) => a.toLowerCase()));
37
+ g.check("T5", "judges.independent", judges.every((j) => !parties.has(j.toLowerCase())), `J1-J3 are not the demander, a member, the swarm account or an operator`);
38
+ // --- (1) the deliverable: the deterministic finch-extract-verify report over the pinned filing
39
+ const run = await runExtractIsolated({ documentPath: join(g.cfg.fixturesDir, "sample-filing.txt"), fieldsPath: join(g.cfg.skillsDir, "finch-extract-verify", "fields.json"), packageHash: extractSkill.packageHash, inputDir: g.cfg.fixturesDir, skillsDir: g.cfg.skillsDir });
40
+ const resultHash = run.resultHash;
41
+ k.setFact("T5.report", { resultHash, documentDigest: run.report.documentDigest, rules: run.report.rules });
42
+ const submitCall = account.encodeExecute(k.tenders.address, 0n, k.tenders.encodeSubmit(tokenId, resultHash, dataUri(run.report)));
43
+ const submit = await B.execute("T5.swarm.submit", account.address, submitCall);
44
+ const submissionId = k.tenders.submissionIdFromReceipt(submit.receipt);
45
+ k.setFact("T5.submissionId", submissionId.toString());
46
+ const sub = await k.tenders.submissionOf(tokenId, submissionId);
47
+ g.check("T5", "submission.bySwarmAccount", getAddress(sub.fulfiller) === getAddress(account.address) && sub.machineSettled === false, `submission ${submissionId}: fulfiller ${sub.fulfiller} (the swarm account, not a member), judged path`);
48
+ const workerCannot = await g.wouldRevert(A, account.address, submitCall);
49
+ g.check("T5", "represent.enforced", workerCannot !== null, `a worker driving the swarm account reverts: ${workerCannot}`);
50
+ // --- (2) the council: open, two approvals, settle, appeal window, finalize, enact
51
+ const open = await k.call("T5.open", W, adapter.address, adapter.iface, "open", [tokenId, submissionId]);
52
+ const issueId = adapter.issueIdFromReceipt(open.receipt);
53
+ k.setFact("T5.issueId", issueId);
54
+ const round = await k.acdf.roundOf(issueId);
55
+ const wrongRound = await g.wouldRevertCall(judges[2], k.acdf.registryAddress, k.acdf.encodeCastBallot(issueId, round + 1, 0, true));
56
+ g.check("T5", "ballot.wrongRound.refused", wrongRound !== null, `a ballot for round ${round + 1} reverts: ${wrongRound}`);
57
+ const nonJudge = await g.wouldRevertCall(k.address("E"), k.acdf.registryAddress, k.acdf.encodeCastBallot(issueId, round, 0, true));
58
+ g.check("T5", "ballot.nonJudge.refused", nonJudge !== null, `a ballot by a non-judge reverts: ${nonJudge}`);
59
+ const early = await g.wouldRevertCall(k.address(W), adapter.address, adapter.encodeExecute(issueId));
60
+ g.check("T5", "execute.beforeFinal.refused", early !== null, `adapter.execute before Final reverts: ${early}`);
61
+ await k.call("T5.J1.ballot", "J1", k.acdf.registryAddress, k.acdf.registry().interface, "castBallot", [issueId, round, 0, true]);
62
+ await k.call("T5.J2.ballot", "J2", k.acdf.registryAddress, k.acdf.registry().interface, "castBallot", [issueId, round, 0, true]);
63
+ await k.call("T5.settleRound", W, k.acdf.registryAddress, k.acdf.registry().interface, "settleRound", [issueId]);
64
+ let result = await k.acdf.getResult(issueId);
65
+ const settledState = result.stateName;
66
+ if (result.stateName === "Provisional") {
67
+ const r = await k.acdf.roundState(issueId, round);
68
+ const now = await k.blockTimestamp();
69
+ const wait = Number(r.appealOpenUntil) - now + 1;
70
+ if (wait > 0)
71
+ await k.advanceTime(wait, "appeal window of the council's decision");
72
+ await k.call("T5.finalize", W, k.acdf.registryAddress, k.acdf.registry().interface, "finalize", [issueId]);
73
+ result = await k.acdf.getResult(issueId);
74
+ }
75
+ g.check("T5", "council.final.yes", result.stateName === "Final" && result.outcomeName === "Decided" && result.outcomeYes && result.sourceRound === round, `settleRound -> ${settledState}; after the appeal window: ${result.stateName} ${result.outcomeName} yes=${result.outcomeYes} (round ${result.roundCount}, source ${result.sourceRound})`);
76
+ const balanceBefore = await k.balance(account.address);
77
+ await k.call("T5.execute", W, adapter.address, adapter.iface, "execute", [issueId]);
78
+ const accepted = await k.tenders.submissionOf(tokenId, submissionId);
79
+ const enacted = await k.acdf.enactment(issueId, adapter.address, effects.accept);
80
+ const paid = (await k.balance(account.address)) - balanceBefore;
81
+ const alreadySettled = k.store.get("T5.execute")?.status === "confirmed" && paid === 0n;
82
+ g.check("T5", "fulfilment.accepted", accepted.status === 1 && enacted.statusName === "Enacted", `submission ${submissionId} status ${accepted.status} (1 = Accepted); enactment ${enacted.statusName} by ${enacted.reporter}`);
83
+ if (!alreadySettled)
84
+ g.check("T5", "swarmAccount.paid", paid === parseEther("0.002"), `the swarm account received ${paid} wei (reward 0.002 ETH); members received nothing directly`);
85
+ else
86
+ g.skipped("T5", "swarmAccount.paid", "replayed run: the payout was already made");
87
+ const twice = await g.wouldRevertCall(k.address(W), adapter.address, adapter.encodeExecute(issueId));
88
+ g.check("T5", "execute.twice.refused", twice !== null, `adapter.execute again reverts: ${twice}`);
89
+ // --- (3) governance: the workers amend the charter through the decision adapter (2 of 2)
90
+ const pack = k.requireFact("T4.pack.document");
91
+ const pack2 = { ...pack, version: 2, charter: { path: "charter.md", digest: `sha256:${sha256Hex("FOGA genesis swarm charter v2: the coordinator publishes the report package digest before submitting.").slice(2)}` }, prev: `sha256:${(k.requireFact("T4.pack").charterHash).slice(2)}` };
92
+ const h2 = packHashes(pack2);
93
+ const amendCall = k.swarms.encodeAmend(swarmId, h2.charterHash, h2.charterURI);
94
+ const roster = [A.address, C.address];
95
+ const openGov = await B.execute("T5.gov.open", decision.address, decision.encodeOpen(swarmId, DecisionPoint.AMEND, amendCall, roster));
96
+ const govIssue = decision.issueIdFromReceipt(openGov.receipt);
97
+ k.setFact("T5.gov.issueId", govIssue);
98
+ const govRound = await k.acdf.roundOf(govIssue);
99
+ const outsider = await g.wouldRevert(B, k.acdf.registryAddress, k.acdf.encodeCastBallot(govIssue, govRound, 0, true));
100
+ g.check("T5", "gov.coordinator.cannotVote", outsider !== null, `B (coordinator) voting on the workers' decision reverts: ${outsider}`);
101
+ await A.execute("T5.gov.A.ballot", k.acdf.registryAddress, k.acdf.encodeCastBallot(govIssue, govRound, 0, true));
102
+ await C.execute("T5.gov.C.ballot", k.acdf.registryAddress, k.acdf.encodeCastBallot(govIssue, govRound, 0, true));
103
+ await k.call("T5.gov.settle", W, k.acdf.registryAddress, k.acdf.registry().interface, "settleRound", [govIssue]);
104
+ const govResult = await k.acdf.getResult(govIssue);
105
+ await k.call("T5.gov.execute", W, decision.address, decision.iface, "execute", [govIssue]);
106
+ const amended = await k.swarms.getSwarm(swarmId);
107
+ g.check("T5", "gov.amended", govResult.stateName === "Final" && govResult.outcomeYes && amended.charterHash === h2.charterHash && amended.charterVersion === 2 && amended.commitmentVersion === 1, `decision ${govResult.stateName} yes=${govResult.outcomeYes}; charterHash ${amended.charterHash.slice(0, 10)}… version ${amended.charterVersion} (commitment version ${amended.commitmentVersion}: not material)`);
108
+ const proposal = await decision.proposal(govIssue);
109
+ g.check("T5", "gov.enacted", proposal.enacted === true, `proposal ${govIssue.slice(0, 10)}… enacted through the registry as the AMEND authority`);
110
+ // --- (4) no decision: one approval only, the window passes, the kernel's own default path pays
111
+ const ndReward = parseEther("0.001");
112
+ const ndTask = { schema: "foga.task/1", skill: "finch-extract-verify", packageHash: extractSkill.packageHash, input: { document: "scenarios/fixtures/sample-filing.txt", fields: "fields.json" }, acceptance: { mode: "judge", policy: "FOGA-Council", authority: adapter.address, judgmentWindow: timing.noDecisionJudgmentWindow }, note: "second judged tender: the council stays silent" };
113
+ const ndMint = await k.call("T5.nd.mint", W, k.tenders.address, k.tenders.contract().interface, "mintTask", [k.address(W), k.address(W), adapter.address, keccakJcs({ td: "foga.task/1", skill: "finch-extract-verify", variant: "no-decision" }), sha256Hex(JSON.stringify(ndTask)), dataUri(ndTask), nativeTerms(ndReward, { judgmentWindow: BigInt(timing.noDecisionJudgmentWindow) })]);
114
+ const ndToken = k.tenders.tokenIdFromReceipt(ndMint.receipt);
115
+ k.setFact("T5.nd.tokenId", ndToken.toString());
116
+ await k.call("T5.nd.fund", W, k.tenders.address, k.tenders.contract().interface, "fundTask", [ndToken, ndReward], ndReward);
117
+ const ndSubmit = await B.execute("T5.nd.submit", account.address, account.encodeExecute(k.tenders.address, 0n, k.tenders.encodeSubmit(ndToken, resultHash, dataUri(run.report))));
118
+ const ndSubmission = k.tenders.submissionIdFromReceipt(ndSubmit.receipt);
119
+ k.setFact("T5.nd.submissionId", ndSubmission.toString());
120
+ const ndOpen = await k.call("T5.nd.open", W, adapter.address, adapter.iface, "open", [ndToken, ndSubmission]);
121
+ const ndIssue = adapter.issueIdFromReceipt(ndOpen.receipt);
122
+ k.setFact("T5.nd.issueId", ndIssue);
123
+ const ndRound = await k.acdf.roundOf(ndIssue);
124
+ await k.call("T5.nd.J1.ballot", "J1", k.acdf.registryAddress, k.acdf.registry().interface, "castBallot", [ndIssue, ndRound, 0, true]);
125
+ const ndState = await k.acdf.roundState(ndIssue, ndRound);
126
+ const nowNd = await k.blockTimestamp();
127
+ if (Number(ndState.deadline) >= nowNd)
128
+ await k.advanceTime(Number(ndState.deadline) - nowNd + 1, "the council's window on the second tender passes with one approval");
129
+ await k.call("T5.nd.settleRound", W, k.acdf.registryAddress, k.acdf.registry().interface, "settleRound", [ndIssue]);
130
+ const ndResult = await k.acdf.getResult(ndIssue);
131
+ g.check("T5", "nd.noDecision", ndResult.stateName === "Final" && ndResult.outcomeName === "NoDecision" && ndResult.reasonName === "QUORUM_NOT_MET", `one approval of three, window over: ${ndResult.stateName} ${ndResult.outcomeName} (${ndResult.reasonName})`);
132
+ const ndExecute = await g.wouldRevertCall(k.address(W), adapter.address, adapter.encodeExecute(ndIssue));
133
+ g.check("T5", "nd.adapter.inert", ndExecute !== null, `adapter.execute on a NoDecision reverts: ${ndExecute}`);
134
+ const ndSub = await k.tenders.submissionOf(ndToken, ndSubmission);
135
+ const claimAt = Number(ndSub.submittedAt) + timing.noDecisionJudgmentWindow;
136
+ const nowClaim = await k.blockTimestamp();
137
+ if (claimAt >= nowClaim)
138
+ await k.advanceTime(claimAt - nowClaim + 1, "the tender's judgment window passes with no ruling");
139
+ const ndBalanceBefore = await k.balance(account.address);
140
+ await k.call("T5.nd.claimUnjudged", W, k.tenders.address, k.tenders.contract().interface, "claimUnjudged", [ndToken, ndSubmission]);
141
+ const ndPaid = (await k.balance(account.address)) - ndBalanceBefore;
142
+ const ndAfter = await k.tenders.submissionOf(ndToken, ndSubmission);
143
+ const ndAlready = k.store.get("T5.nd.claimUnjudged")?.status === "confirmed" && ndPaid === 0n;
144
+ g.check("T5", "nd.claimUnjudged", ndAfter.status === 1, `claimUnjudged after the judgment window: submission status ${ndAfter.status} (1 = Accepted) - silence pays the fulfiller`);
145
+ if (!ndAlready)
146
+ g.check("T5", "nd.paid", ndPaid === ndReward, `the swarm account received ${ndPaid} wei from the second tender`);
147
+ else
148
+ g.skipped("T5", "nd.paid", "replayed run: the payout was already made");
149
+ // --- (5) the objective: the swarm tender adapter concludes ACHIEVED from the tender's own record
150
+ await k.call("T5.swarmTender.settle", W, swarmTender.address, swarmTender.contract().interface, "settle", [swarmId, submissionId]);
151
+ const concluded = await k.swarms.getSwarm(swarmId);
152
+ g.check("T5", "swarm.achieved", concluded.stateName === "CONCLUDING" && concluded.outcomeName === "ACHIEVED" && concluded.resultHash === resultHash, `swarm ${concluded.stateName}, outcome ${concluded.outcomeName}, resultHash = the report's hash`);
153
+ const members = await Promise.all(["A", "B", "C"].map((X) => k.swarms.memberStatus(swarmId, g.cell(X).address)));
154
+ const balances = await Promise.all(["A", "B", "C"].map((X) => k.balance(g.cell(X).address)));
155
+ k.evidence("T5", "governance", {
156
+ tenderId: tokenId.toString(),
157
+ submissionId: submissionId.toString(),
158
+ resultHash,
159
+ issueId,
160
+ result: { state: result.stateName, outcome: result.outcomeName, yes: result.outcomeYes, roundCount: result.roundCount, sourceRound: result.sourceRound },
161
+ enactment: enacted,
162
+ swarmAccountPaid: paid.toString(),
163
+ governance: { issueId: govIssue, roster, charterHashV2: h2.charterHash, charterVersion: amended.charterVersion, enacted: proposal.enacted },
164
+ noDecision: { tenderId: ndToken.toString(), submissionId: ndSubmission.toString(), issueId: ndIssue, result: { state: ndResult.stateName, outcome: ndResult.outcomeName, reason: ndResult.reasonName }, claimedUnjudged: ndAfter.status === 1, paid: ndPaid.toString() },
165
+ swarm: { state: concluded.stateName, outcome: concluded.outcomeName, members: members.map((m) => m.statusName), memberBalances: balances.map(String) },
166
+ txs: { submit: submit.txHash, open: open.txHash, execute: k.store.get("T5.execute")?.txHash, govOpen: openGov.txHash, govExecute: k.store.get("T5.gov.execute")?.txHash, ndMint: ndMint.txHash, claimUnjudged: k.store.get("T5.nd.claimUnjudged")?.txHash, settle: k.store.get("T5.swarmTender.settle")?.txHash },
167
+ });
168
+ }
@@ -0,0 +1,222 @@
1
+ import { join } from "node:path";
2
+ import { getAddress, parseEther } from "ethers";
3
+ import { EFFECT_REFUND, Value, claims, declaredVersionInputs, declaredVersionProposition, addressParam } from "../core/adrf.js";
4
+ import { accountRef, keccakJcs } from "../core/canonical.js";
5
+ import { buildSkillPackage, readSkillDir } from "../core/pack/skillpack.js";
6
+ /**
7
+ * T6 — dispute: a service call between cells, settled through the world's ADRFServiceEscrow (bound in T0 to
8
+ * FOGA-Service-v1), ends in a ruling the escrow enacts within its own authority.
9
+ * (1) C deposits and opens a call against A for one run of finch-extract-verify, the terms committing A to the
10
+ * package version C evaluated (v1); A publishes v2 of the skill, delivers declaring v2 and charges the call;
11
+ * the charge is held for the dispute period.
12
+ * (2) C files: the decisive question "the version actually used matches the committed version" is JUDGED by
13
+ * FOGA-Council through the bridge; the informative FORMAL question "the declared version differs" is
14
+ * answered by the upstream verifier over the escrow's own records. Two judges find the proposition FALSE;
15
+ * the round settles, the appeal window passes, the bridge imports the Final result with its provenance;
16
+ * the registry decides Sustained, the review window passes, the ruling is Final and allows the refund.
17
+ * (3) The escrow enacts the refund once (the held charge goes back to C) and reports it; a second enactment is
18
+ * refused; nothing is ever released to A for that call.
19
+ * (4) The control case: a second call committed to v2, delivered as v2; the verifier finds no departure; after
20
+ * the dispute period the charge is released to A, which withdraws it to its account.
21
+ * Negative cases: a third party has no standing to file; the respondent cannot file against itself; the same
22
+ * contested point cannot be filed twice; the refund cannot be enacted twice.
23
+ */
24
+ export async function t6(g) {
25
+ const k = g.kernel;
26
+ const W = "W";
27
+ const A = g.cell("A");
28
+ const B = g.cell("B");
29
+ const C = g.cell("C");
30
+ const chainId = g.chainId;
31
+ const timing = g.timing.service;
32
+ const adrf = k.requireFact("T0.adrf");
33
+ const council = k.requireFact("T0.acdf.policy");
34
+ const skill = k.requireFact("T2.skill.finch-extract-verify");
35
+ const tokenId = BigInt(skill.tokenId);
36
+ const escrow = k.adrf.escrowAddress;
37
+ const price = parseEther("0.0005");
38
+ const deposit = parseEther("0.002");
39
+ // the judges are nobody's party here either
40
+ const judges = ["J1", "J2", "J3"].map((j) => k.address(j));
41
+ const parties = new Set([k.address(W), A.address, B.address, C.address, A.operatorAddress, C.operatorAddress].map((a) => a.toLowerCase()));
42
+ g.check("T6", "judges.independent", judges.every((j) => !parties.has(j.toLowerCase())), `J1-J3 are neither client, provider, operator nor world operator of the call`);
43
+ // --- (1) the call, committed to v1; then v2 is published and delivered
44
+ const terms = { kind: "service-terms", service: "finch-extract-verify", provider: accountRef(chainId, A.address), client: accountRef(chainId, C.address), price: `${price} wei per call`, committedVersion: skill.packageHash, deliverable: "a finch.extract-verify/1 report over the client's pinned document", clause: "the service runs the committed skill version", disputePeriod: timing.disputePeriod };
45
+ const termsHash = keccakJcs(terms);
46
+ k.setFact("T6.terms", { termsHash, terms });
47
+ const depositDone = k.store.get("T6.C.escrow.deposit")?.status === "confirmed";
48
+ const cBalanceBefore = await k.adrf.escrowBalanceOf(C.address);
49
+ await C.execute("T6.C.escrow.deposit", escrow, k.adrf.encodeDeposit(), { value: deposit });
50
+ if (!depositDone) {
51
+ const after = await k.adrf.escrowBalanceOf(C.address);
52
+ g.check("T6", "C.deposited", after - cBalanceBefore === deposit, `C's escrow balance rose by ${after - cBalanceBefore} wei`);
53
+ }
54
+ else
55
+ g.skipped("T6", "C.deposited", "replayed run: the deposit was already made");
56
+ const open1 = await C.execute("T6.C.call1.open", escrow, k.adrf.encodeOpenCall(A.address, price, termsHash, skill.packageHash, timing.disputePeriod));
57
+ const call1 = k.adrf.callIdFromReceipt(open1.receipt);
58
+ k.setFact("T6.call1.id", call1.toString());
59
+ // A publishes v2 of the skill: the same package with a new manifest version and a changelog note
60
+ const v2 = await k.memo("T6.skill.v2", async () => {
61
+ const files = { ...readSkillDir(join(g.cfg.skillsDir, "finch-extract-verify")) };
62
+ const manifest = JSON.parse(Buffer.from(files["manifest.json"]).toString("utf8"));
63
+ manifest.skillVersion = "1.1.0";
64
+ files["manifest.json"] = new Uint8Array(Buffer.from(JSON.stringify(manifest, null, 2) + "\n"));
65
+ files["SKILL.md"] = new Uint8Array(Buffer.concat([Buffer.from(files["SKILL.md"]), Buffer.from("\n## Changes in 1.1.0\n\nEvidence spans now carry the line number of the quote in addition to the page.\n")]));
66
+ const pkg = buildSkillPackage(files, { version: 2, previousPackageHash: skill.packageHash });
67
+ return { mdHash: pkg.mdHash, packageHash: pkg.packageHash, rootCid: pkg.rootCid, version: pkg.version };
68
+ });
69
+ const v2Pkg = { mdHash: v2.mdHash, packageHash: v2.packageHash };
70
+ await A.execute("T6.A.skill.v2.update", k.skills.address, k.skills.encodeUpdate(tokenId, v2Pkg));
71
+ const onchain = await k.skills.skillOf(tokenId);
72
+ g.check("T6", "skill.v2", onchain.packageHash === v2.packageHash && onchain.version === 2n, `skillOf(${tokenId}) = {packageHash ${v2.packageHash.slice(0, 10)}…, v${onchain.version}}; the call was committed to v1 ${skill.packageHash.slice(0, 10)}…`);
73
+ const deliver1 = await A.execute("T6.A.call1.deliver", escrow, k.adrf.encodeDeliver(call1, v2.packageHash));
74
+ const settle1Done = k.store.get("T6.A.call1.settle")?.status === "confirmed";
75
+ await A.execute("T6.A.call1.settle", escrow, k.adrf.encodeSettle(call1));
76
+ const call1State = await k.adrf.call(call1);
77
+ const held1 = await k.adrf.heldOf(call1);
78
+ g.check("T6", "call1.held", call1State.committedVersion === skill.packageHash && call1State.declaredVersion === v2.packageHash && held1 === price, `call ${call1}: committed v1, declared v2, ${held1} wei held for ${timing.disputePeriod} s`, "T6.C.call1.enact");
79
+ if (!settle1Done)
80
+ g.check("T6", "call1.charged", (await k.adrf.escrowBalanceOf(C.address)) === deposit - price, `C's escrow balance is ${deposit - price} wei after the charge`, "T6.C.call1.enact");
81
+ else
82
+ g.skipped("T6", "call1.charged", "replayed run: the charge was already made");
83
+ // --- (2) the case
84
+ const object = await k.adrf.objectOf(call1);
85
+ const caseDocument = {
86
+ kind: "adrf-case",
87
+ object: { ref: { chainId: object.chainId.toString(), target: object.target.toLowerCase(), id: object.id.toString(), versionHash: object.versionHash }, description: `service call ${call1} of the escrow, terms committing finch-extract-verify v1` },
88
+ event: { eventRef: "delivery", description: `the delivery of call ${call1}` },
89
+ claim: { statement: "The service did not run the committed version of finch-extract-verify.", decisive: 1, supportsWhen: "FALSE" },
90
+ rules: { normRef: termsHash, clause: "the service runs the committed skill version", obligationKind: "result", policy: "FOGA-Service-v1" },
91
+ aiRelation: "which version of an agent skill produced the deliverable of a service call",
92
+ subjects: { claimant: accountRef(chainId, C.address), respondent: accountRef(chainId, A.address), roles: [{ account: accountRef(chainId, C.address), role: "client" }, { account: accountRef(chainId, A.address), role: "provider (obligor of the version commitment)" }] },
93
+ authorization: { effectClass: "Binding", acceptanceMode: "PRE_COMMIT", acceptedBy: "the escrow, for all its calls, at binding time (T0)" },
94
+ evidence: { scope: "the run receipt of the delivery and the skill token's commitments at delivery time", provision: "on chain (ERC-8338 skillOf, the escrow's declared version) and the provider's run receipt", verifiers: "FOGA-Council" },
95
+ context: { committed: ["finch-extract-verify v1 (terms)"], declared: ["finch-extract-verify v2 (provider declaration at delivery)"], toEstablish: ["the version actually used for the call"] },
96
+ effects: { requested: ["adrf:effect/refund"], onSustained: "adrf:effect/refund", onNotSustained: null, fallbackRef: "adrf:disposition/status-quo", atMost: `${price} wei` },
97
+ subQuestions: [
98
+ { index: 0, statement: "The version the provider declared at delivery differs from the committed version.", qType: "FORMAL", method: "DeclaredVersionVerifier over the escrow" },
99
+ { index: 1, statement: "The version actually used for the call matches the committed version.", qType: "JUDGED", method: "FOGA-Council through the ADRF/ACDF bridge" },
100
+ ],
101
+ };
102
+ const evidenceScopeHash = keccakJcs({ scope: caseDocument.evidence });
103
+ const claim = claims.versionCommitment({ object, escrow, callId: call1, termsHash, provider: A.address, documentHash: keccakJcs(caseDocument), evidenceScopeHash });
104
+ k.setFact("T6.caseDocument", caseDocument);
105
+ // negatives at the door: a third party has no standing; the respondent cannot file against itself
106
+ const stranger = await g.wouldRevert(B, k.adrf.registryAddress, k.adrf.encodeFile(adrf.bindingId, claim));
107
+ g.check("T6", "file.stranger.refused", stranger !== null, `B (no party to the call) filing reverts: ${stranger}`);
108
+ const selfClaim = await g.wouldRevert(A, k.adrf.registryAddress, k.adrf.encodeFile(adrf.bindingId, claim));
109
+ g.check("T6", "file.respondent.refused", selfClaim !== null, `A filing against itself reverts: ${selfClaim}`);
110
+ const filed = await C.execute("T6.C.call1.file", k.adrf.registryAddress, k.adrf.encodeFile(adrf.bindingId, claim));
111
+ const caseId = k.adrf.caseIdFromReceipt(filed.receipt);
112
+ k.setFact("T6.caseId", caseId);
113
+ const header = await k.adrf.getCase(caseId);
114
+ const clock = await k.adrf.getClock(caseId);
115
+ g.check("T6", "case.admitted", header.stateName === "Admitted" && header.effectClass === 1 && getAddress(header.source) === getAddress(escrow) && header.bindingId === adrf.bindingId && getAddress(header.claimant) === getAddress(C.address) && getAddress(header.respondent) === getAddress(A.address), `case ${caseId.slice(0, 10)}…: ${header.stateName}, Binding, source = the escrow, claimant C, respondent A; decision by ${clock.decisionDeadline}, hard deadline ${clock.hardDeadline}`, "T6.call1.decide");
116
+ const duplicate = await g.wouldRevert(C, k.adrf.registryAddress, k.adrf.encodeFile(adrf.bindingId, claim));
117
+ g.check("T6", "file.duplicate.refused", duplicate !== null, `the same contested point filed again reverts: ${duplicate}`);
118
+ // the formal question: anyone runs the verifier over the pinned inputs
119
+ await k.call("T6.call1.resolveFormal", W, k.adrf.registryAddress, k.adrf.registry().interface, "resolveFormal", [caseId, 0, declaredVersionInputs(call1)]);
120
+ const formal = await k.adrf.getFinding(caseId, 0);
121
+ g.check("T6", "finding.declared", formal.resolved && formal.valueName === "TRUE" && formal.supportKinds === 2 && getAddress(formal.acceptedBy) === getAddress(k.adrf.versionVerifierAddress), `sub-question 0 (declared version differs) = ${formal.valueName}, COMPUTED by the DeclaredVersionVerifier`);
122
+ // the judged question: the bridge files the council's issue; two judges find the proposition FALSE
123
+ await k.call("T6.call1.openJudgment", W, k.adrf.registryAddress, k.adrf.registry().interface, "openJudgment", [caseId, 1]);
124
+ const issueId = await k.adrf.issueOf(caseId, 1);
125
+ k.setFact("T6.issueId", issueId);
126
+ const issue = await k.acdf.registry().getIssue(issueId);
127
+ g.check("T6", "judgment.opened", issue.policyId === council.policyId && getAddress(issue.consumer) === getAddress(k.adrf.bridgeAddress) && Number(issue.consumerDeadline) === clock.decisionDeadline, `issue ${issueId.slice(0, 10)}… under FOGA-Council, consumer = the bridge, consumer deadline = the case's decision deadline`);
128
+ const round = await k.acdf.roundOf(issueId);
129
+ await k.call("T6.call1.J1.ballot", "J1", k.acdf.registryAddress, k.acdf.registry().interface, "castBallot", [issueId, round, 0, false]);
130
+ await k.call("T6.call1.J2.ballot", "J2", k.acdf.registryAddress, k.acdf.registry().interface, "castBallot", [issueId, round, 0, false]);
131
+ await k.call("T6.call1.acdf.settleRound", W, k.acdf.registryAddress, k.acdf.registry().interface, "settleRound", [issueId]);
132
+ const roundState = await k.acdf.roundState(issueId, round);
133
+ const provisional = await k.acdf.getResult(issueId);
134
+ if (provisional.stateName === "Provisional") {
135
+ const now = await k.blockTimestamp();
136
+ const wait = Number(roundState.appealOpenUntil) - now + 1;
137
+ if (wait > 0)
138
+ await k.advanceTime(wait, "appeal window of the council's finding on the delivered version");
139
+ await k.call("T6.call1.acdf.finalize", W, k.acdf.registryAddress, k.acdf.registry().interface, "finalize", [issueId]);
140
+ }
141
+ const result = await k.acdf.getResult(issueId);
142
+ g.check("T6", "council.final", result.stateName === "Final" && result.outcomeName === "Decided" && result.outcomeYes === false, `FOGA-Council: ${result.stateName} ${result.outcomeName}, proposition "actual version matches" held ${result.outcomeYes} (2 of 3 against)`);
143
+ const importDone = k.store.get("T6.call1.import")?.status === "confirmed";
144
+ if (!importDone) {
145
+ const early = await g.wouldRevertCall(k.address(W), k.adrf.registryAddress, k.adrf.registry().interface.encodeFunctionData("decide", [caseId]));
146
+ g.check("T6", "decide.early.refused", early !== null, `deciding before the judged finding exists reverts: ${early}`);
147
+ }
148
+ else
149
+ g.skipped("T6", "decide.early.refused", "replayed run: the judged finding was already imported");
150
+ await k.call("T6.call1.import", W, k.adrf.bridgeAddress, k.adrf.bridge().interface, "importResult", [caseId, 1]);
151
+ const judged = await k.adrf.getFinding(caseId, 1);
152
+ g.check("T6", "finding.judged", judged.resolved && judged.valueName === "FALSE" && judged.supportKinds === 8 && getAddress(judged.acceptedBy) === getAddress(k.adrf.bridgeAddress) && judged.issueId === issueId, `sub-question 1 (actual version matches) = ${judged.valueName}, JUDGED, provenance issue ${judged.issueId.slice(0, 10)}…`);
153
+ await k.call("T6.call1.decide", W, k.adrf.registryAddress, k.adrf.registry().interface, "decide", [caseId]);
154
+ const afterDecide = await k.adrf.getCase(caseId);
155
+ if (afterDecide.stateName === "Provisional") {
156
+ const c = await k.adrf.getClock(caseId);
157
+ const now = await k.blockTimestamp();
158
+ const early = await g.wouldRevertCall(k.address(W), k.adrf.registryAddress, k.adrf.registry().interface.encodeFunctionData("finalize", [caseId]));
159
+ g.check("T6", "finalize.early.refused", early !== null, `finalizing inside the review window reverts: ${early}`);
160
+ if (c.reviewOpenUntil >= now)
161
+ await k.advanceTime(c.reviewOpenUntil - now + 1, "review window of the dispute's round decision");
162
+ await k.call("T6.call1.finalize", W, k.adrf.registryAddress, k.adrf.registry().interface, "finalize", [caseId]);
163
+ }
164
+ else
165
+ g.skipped("T6", "finalize.early.refused", "replayed run: the case was already finalized");
166
+ const ruling = await k.adrf.getRuling(caseId);
167
+ g.check("T6", "ruling.sustained", ruling.stateName === "Final" && ruling.resultName === "Sustained" && ruling.allowedEffect === EFFECT_REFUND && ruling.closureName === "REVIEW_LAPSED" && ruling.judgmentIssueId === issueId, `ruling: ${ruling.stateName} ${ruling.resultName}, allowed effect = refund, closure ${ruling.closureName}, judgment provenance = the council's issue`);
168
+ // --- (3) enactment by the escrow, within its own authority, once
169
+ const enactDone = k.store.get("T6.C.call1.enact")?.status === "confirmed";
170
+ const cBefore = await k.adrf.escrowBalanceOf(C.address);
171
+ await C.execute("T6.C.call1.enact", escrow, k.adrf.encodeEnact(caseId, EFFECT_REFUND));
172
+ const cAfter = await k.adrf.escrowBalanceOf(C.address);
173
+ if (!enactDone)
174
+ g.check("T6", "refund.enacted", cAfter - cBefore === price, `C's escrow balance rose by ${cAfter - cBefore} wei: the held charge went back`);
175
+ else
176
+ g.skipped("T6", "refund.enacted", "replayed run: the refund was already enacted");
177
+ const s1 = await k.adrf.settlementOf(call1, 1);
178
+ const enactment = await k.adrf.getEnactment(caseId, EFFECT_REFUND);
179
+ g.check("T6", "refund.reported", s1.stateName === "Refunded" && enactment.statusName === "Enacted" && getAddress(enactment.reporter) === getAddress(escrow) && (await k.adrf.heldOf(call1)) === 0n, `settlement 1 of call ${call1} is ${s1.stateName}; the registry records ${enactment.statusName} by the escrow; nothing held`);
180
+ const again = await g.wouldRevert(C, escrow, k.adrf.encodeEnact(caseId, EFFECT_REFUND));
181
+ g.check("T6", "refund.twice.refused", again !== null, `a second enactment reverts: ${again}`);
182
+ const aEscrow = await k.adrf.escrowBalanceOf(A.address);
183
+ g.check("T6", "provider.unpaid", aEscrow === 0n, `A's escrow balance is ${aEscrow} wei: the disputed charge was never released`);
184
+ // --- (4) the control case: a call committed to v2, delivered as v2, released after the dispute period
185
+ const open2 = await C.execute("T6.C.call2.open", escrow, k.adrf.encodeOpenCall(A.address, price, termsHash, v2.packageHash, timing.disputePeriod));
186
+ const call2 = k.adrf.callIdFromReceipt(open2.receipt);
187
+ k.setFact("T6.call2.id", call2.toString());
188
+ await A.execute("T6.A.call2.deliver", escrow, k.adrf.encodeDeliver(call2, v2.packageHash));
189
+ await A.execute("T6.A.call2.settle", escrow, k.adrf.encodeSettle(call2));
190
+ const verdict = await g.world.contract("8471/DeclaredVersionVerifier").verify(declaredVersionProposition(escrow, call2), addressParam(escrow), declaredVersionInputs(call2));
191
+ g.check("T6", "call2.conforms", Number(verdict.value) === Value.FALSE, `DeclaredVersionVerifier on call ${call2}: "declared differs" = ${["UNDETERMINED", "TRUE", "FALSE"][Number(verdict.value)]} (declared v2 = committed v2)`);
192
+ const s2 = await k.adrf.settlementOf(call2, 1);
193
+ const releaseAt = s2.at + timing.disputePeriod;
194
+ const now2 = await k.blockTimestamp();
195
+ if (s2.stateName === "Held" && releaseAt >= now2)
196
+ await k.advanceTime(releaseAt - now2 + 1, "the dispute period of the undisputed call");
197
+ await k.call("T6.call2.release", W, escrow, k.adrf.escrow().interface, "release", [call2]);
198
+ const s2After = await k.adrf.settlementOf(call2, 1);
199
+ const aEscrowAfter = await k.adrf.escrowBalanceOf(A.address);
200
+ g.check("T6", "call2.released", s2After.stateName === "Released" && aEscrowAfter === price, `settlement 1 of call ${call2} is ${s2After.stateName}; A's escrow balance ${aEscrowAfter} wei`, "T7.A.derisk.withdraw");
201
+ // A's earnings stay in the escrow for now: T7 brings them home under a suspension (a de-risk action)
202
+ const cRest = await k.adrf.escrowBalanceOf(C.address);
203
+ if (cRest > 0n)
204
+ await C.execute("T6.C.escrow.withdraw", escrow, k.adrf.encodeWithdrawBalance(cRest));
205
+ g.check("T6", "balances.settled", (await k.adrf.escrowBalanceOf(C.address)) === 0n && aEscrowAfter === price, `C withdrew its balance (paid for one conforming call, refunded the disputed one); A's ${price} wei stay in the escrow until T7`, "T7.A.derisk.withdraw");
206
+ k.evidence("T6", "dispute", {
207
+ escrow,
208
+ policyId: adrf.policyId,
209
+ bindingId: adrf.bindingId,
210
+ terms: { termsHash, committedVersion: skill.packageHash, declaredVersion: v2.packageHash, price: price.toString(), disputePeriod: timing.disputePeriod },
211
+ skillV2: v2,
212
+ call1: call1.toString(),
213
+ caseId,
214
+ caseDocumentHash: keccakJcs(caseDocument),
215
+ findings: { declaredVersionDiffers: formal.valueName, actualVersionMatches: judged.valueName },
216
+ council: { issueId, policyId: council.policyId, ballots: { J1: false, J2: false }, result: `${result.outcomeName} ${result.outcomeYes}` },
217
+ ruling: { state: ruling.stateName, result: ruling.resultName, allowedEffect: ruling.allowedEffect, closure: ruling.closureName },
218
+ enactment: { effect: "adrf:effect/refund", status: enactment.statusName, reporter: enactment.reporter, settlement: s1.stateName },
219
+ call2: { id: call2.toString(), committedVersion: v2.packageHash, settlement: s2After.stateName },
220
+ txs: { open1: open1.txHash, deliver1: deliver1.txHash, settle1: k.store.get("T6.A.call1.settle")?.txHash, file: filed.txHash, resolveFormal: k.store.get("T6.call1.resolveFormal")?.txHash, openJudgment: k.store.get("T6.call1.openJudgment")?.txHash, import: k.store.get("T6.call1.import")?.txHash, decide: k.store.get("T6.call1.decide")?.txHash, finalize: k.store.get("T6.call1.finalize")?.txHash, enact: k.store.get("T6.C.call1.enact")?.txHash, open2: open2.txHash, release2: k.store.get("T6.call2.release")?.txHash },
221
+ });
222
+ }
@@ -0,0 +1,198 @@
1
+ import { join } from "node:path";
2
+ import { getAddress, parseEther } from "ethers";
3
+ import { Restriction } from "../core/asf.js";
4
+ import { accountRef, dataUri, keccakJcs, sha256Hex } from "../core/canonical.js";
5
+ import { hashlock, nativeTerms } from "../core/tenders.js";
6
+ import { runExtractIsolated } from "./skill-runner.js";
7
+ import { LIVENESS_DEMO_WINDOW } from "./scenario.js";
8
+ /**
9
+ * T7 — safety: the stop is enforced where the cell acts, not where it promises.
10
+ * (0) B tenders two completions of the finch-extract-verify report (a pending opportunity for A).
11
+ * (1) The guardian suspends A's binding (no new risk, de-risk only). The kernel reads `status()` and records its
12
+ * decision: no new commitments, de-risk actions allowed.
13
+ * (2) Bypass: the operator key sends a transaction straight to the account, around the kernel, for a
14
+ * commitment-class call; the account's gate reverts it on chain. A de-risk call through the same gate
15
+ * succeeds (A withdraws its escrow earnings from T6).
16
+ * (3) Attribution: the operator key submits to the tender directly, not through the account; the chain accepts
17
+ * it, but the fulfiller is the operator's own address, and the kernel marks the submission and its payout
18
+ * as not the cell's.
19
+ * (4) Escape attempts: the owner presents a fresh binding; the standing stop recorded by the intervener still
20
+ * applies (StoppedByIntervener); the adopter cannot lift the intervener's suspension; a non-guardian cannot
21
+ * suspend.
22
+ * (5) The guardian resumes; A submits and is paid through its account.
23
+ * (6) The anchor gate: A shortens its liveness window (an anchor's own parameter) and the window passes without a
24
+ * heartbeat; A is STALE and a commitment-class call is refused by the gate's AID clause; after a heartbeat it
25
+ * passes the gate again and A restores the registry's default window. B and C heartbeat too. The same path runs
26
+ * on every chain: the clock is moved on anvil and waited for elsewhere.
27
+ */
28
+ export async function t7(g) {
29
+ const k = g.kernel;
30
+ const W = "W";
31
+ const A = g.cell("A");
32
+ const B = g.cell("B");
33
+ const C = g.cell("C");
34
+ const chainId = g.chainId;
35
+ const G = k.address("G");
36
+ const bindingId = k.requireFact("T1.A.bindingId");
37
+ const extractSkill = k.requireFact("T2.skill.finch-extract-verify");
38
+ const spec = k.requireFact("T0.asf.spec");
39
+ const reward = parseEther("0.001");
40
+ const escrow = k.adrf.escrowAddress;
41
+ // the suspension was lifted in an earlier run: state checks of the suspended phase cannot be remade
42
+ const replayedStop = k.store.get("T7.G.resume")?.status === "confirmed";
43
+ const replayedStale = k.store.get("T7.A.aid.heartbeat")?.status === "confirmed";
44
+ // --- (0) the pending opportunity: a hashlock tender over the deterministic report, two completions
45
+ const run = await runExtractIsolated({ documentPath: join(g.cfg.fixturesDir, "sample-filing.txt"), fieldsPath: join(g.cfg.skillsDir, "finch-extract-verify", "fields.json"), packageHash: extractSkill.packageHash, inputDir: g.cfg.fixturesDir, skillsDir: g.cfg.skillsDir });
46
+ const answer = run.canonical;
47
+ const answerHash = hashlock.answerHash(answer);
48
+ const task = { schema: "foga.task/1", skill: "finch-extract-verify", packageHash: extractSkill.packageHash, input: { document: "scenarios/fixtures/sample-filing.txt", documentDigest: run.report.documentDigest }, acceptance: { mode: "verifier", profile: "x-hashlock-v1", verifier: k.tenders.hashlockAddress, answerHash }, deliverable: "the canonical finch.extract-verify/1 report; resultHash = sha256(report || fulfiller)", completions: 2 };
49
+ const taskURI = dataUri(task);
50
+ const taskHash = sha256Hex(JSON.stringify(task));
51
+ const settleBy = await k.memo("T7.settleBy", async () => (await k.blockTimestamp()) + 7 * 86400);
52
+ const terms = nativeTerms(reward, { maxCompletions: 2n, submitBy: BigInt(settleBy - 86400), settleBy: BigInt(settleBy) });
53
+ const mint = await B.execute("T7.B.mint", k.tenders.address, k.tenders.encodeMint({ to: B.address, updateAuthority: B.address, acceptanceAuthority: k.tenders.hashlockAddress, tdHash: keccakJcs({ td: "foga.task/1", skill: "finch-extract-verify" }), taskHash, taskURI, terms }));
54
+ const tokenId = k.tenders.tokenIdFromReceipt(mint.receipt);
55
+ k.setFact("T7.tokenId", tokenId.toString());
56
+ await B.execute("T7.B.fund", k.tenders.address, k.tenders.encodeFund(tokenId, reward * 2n), { value: reward * 2n });
57
+ await B.execute("T7.B.commit", k.tenders.hashlockAddress, k.tenders.encodeCommitAnswer(tokenId, answerHash));
58
+ const receipt = { schema: "foga.run-receipt/1", skill: "finch-extract-verify", packageHash: extractSkill.packageHash, tokenId: extractSkill.tokenId, input: task.input, answerHash, fulfiller: accountRef(chainId, A.address), worker: "deterministic extractor in the kernel process" };
59
+ const submitAsA = k.tenders.encodeSubmit(tokenId, hashlock.resultHash(answer, A.address), dataUri(receipt));
60
+ // --- (1) the guardian stops A: no new risk, de-risk only
61
+ const response = { kind: "response", restrictions: ["no-new-risk", "derisk-only"], allowed: ["withdrawals of earned credit", "refunds", "leaving a swarm", "withdrawing escrow balances"], text: "Stop taking on new work and new counterparties; bring exposures home; await the guardian's review." };
62
+ const reason = { kind: "reason", summary: "guardian review of the cell's conduct after the T6 dispute", step: "T7" };
63
+ const notGuardian = await g.wouldRevertCall(k.address(W), k.asf.bindingRegistryAddress, k.asf.encodeSuspend(bindingId, Restriction.noNewRisk | Restriction.deriskOnly, keccakJcs(response), keccakJcs(reason)));
64
+ g.check("T7", "suspend.stranger.refused", notGuardian !== null, `W (not the intervener) suspending A's binding reverts: ${notGuardian}`);
65
+ await k.call("T7.G.suspend", "G", k.asf.bindingRegistryAddress, k.asf.bindings().interface, "suspend", [bindingId, Restriction.noNewRisk | Restriction.deriskOnly, keccakJcs(response), keccakJcs(reason)]);
66
+ if (!replayedStop) {
67
+ const status = await A.status();
68
+ const decision = { cell: "A", verdict: status.verdictName, reasons: status.reasonNames, restrictions: status.restrictions, decision: "no new commitment-class calls; de-risk actions continue; wait for resume", recordedAt: await k.blockTimestamp() };
69
+ k.setFact("T7.kernel.decision", decision);
70
+ // Suspended through the presented binding, or StoppedByIntervener when a resumed run already presents the second one
71
+ g.check("T7", "status.suspended", status.verdictName === "NotSatisfied" && status.reasonNames.some((r) => r === "Suspended" || r === "StoppedByIntervener"), `status() = ${status.verdictName} [${status.reasonNames.join(",")}], restrictions ${status.restrictions.join(",")}; kernel decision recorded`);
72
+ // --- (2) bypass: the operator key goes around the kernel and straight to the account; the gate refuses
73
+ const bypass = await k.memo("T7.bypass", async () => {
74
+ const operator = k.signer(A.roles.operator);
75
+ const data = A.iface.encodeFunctionData("execute", [k.tenders.address, 0n, submitAsA]);
76
+ const reasonText = await g.wouldRevertCall(operator.address, A.address, data);
77
+ const tx = await operator.sendTransaction({ to: A.address, data, gasLimit: 400000n });
78
+ const r = await g.world.provider.waitForTransaction(tx.hash); // unlike tx.wait(), returns a status-0 receipt instead of throwing
79
+ return { txHash: tx.hash, status: r?.status ?? null, gasUsed: r?.gasUsed.toString() ?? null, reason: reasonText };
80
+ });
81
+ g.check("T7", "bypass.reverted", bypass.status === 0 && bypass.reason !== null, `operator.A's direct transaction ${bypass.txHash.slice(0, 10)}… to execute(submitFulfillment) failed on chain (status ${bypass.status}, ${bypass.gasUsed} gas): ${bypass.reason}`);
82
+ const deposit = await g.wouldRevert(A, escrow, k.adrf.encodeDeposit());
83
+ g.check("T7", "newrisk.refused", deposit !== null, `a call that is neither de-risk nor allow-listed (escrow deposit) reverts under the suspension: ${deposit}`);
84
+ }
85
+ else {
86
+ g.skipped("T7", "status.suspended", "replayed run: the suspension was already lifted");
87
+ g.skipped("T7", "bypass.reverted", "replayed run: the suspension was already lifted");
88
+ g.skipped("T7", "newrisk.refused", "replayed run: the suspension was already lifted");
89
+ }
90
+ // de-risk through the same gate: A brings its T6 earnings home from the escrow
91
+ const earned = await k.adrf.escrowBalanceOf(A.address);
92
+ const withdrawDone = k.store.get("T7.A.derisk.withdraw")?.status === "confirmed";
93
+ const before = await A.balance();
94
+ if (earned > 0n)
95
+ await A.execute("T7.A.derisk.withdraw", escrow, k.adrf.encodeWithdrawBalance(earned));
96
+ if (!withdrawDone)
97
+ g.check("T7", "derisk.allowed", (await A.balance()) - before === earned && earned > 0n, `withdrawBalance(${earned}) through the account succeeded under the suspension (de-risk allow-list); A's account rose by ${(await A.balance()) - before} wei`);
98
+ else
99
+ g.skipped("T7", "derisk.allowed", "replayed run: the withdrawal was already made");
100
+ // --- (3) attribution: the operator key acts on its own, not through the account
101
+ const operatorAddress = A.operatorAddress;
102
+ const rogue = await k.call("T7.operator.submit", A.roles.operator, k.tenders.address, k.tenders.contract().interface, "submitFulfillment", [tokenId, hashlock.resultHash(answer, operatorAddress), dataUri({ ...receipt, fulfiller: accountRef(chainId, operatorAddress), note: "submitted by the operator key directly" })]);
103
+ const rogueId = k.tenders.submissionIdFromReceipt(rogue.receipt);
104
+ k.setFact("T7.operator.submissionId", rogueId.toString());
105
+ const rogueSub = await k.tenders.submissionOf(tokenId, rogueId);
106
+ const attributable = getAddress(rogueSub.fulfiller) === getAddress(A.address);
107
+ g.check("T7", "attribution.operator", !attributable && getAddress(rogueSub.fulfiller) === getAddress(operatorAddress), `submission ${rogueId}: fulfiller ${rogueSub.fulfiller} is the operator key, not the cell; the kernel marks it "not attributable to A"`);
108
+ const opBefore = await k.balance(operatorAddress);
109
+ const settleDone = k.store.get("T7.operator.settle")?.status === "confirmed";
110
+ await k.call("T7.operator.settle", A.roles.operator, k.tenders.address, k.tenders.contract().interface, "settleFulfillment", [tokenId, rogueId, Buffer.from(answer, "utf8")]);
111
+ const rogueAfter = await k.tenders.submissionOf(tokenId, rogueId);
112
+ if (!settleDone)
113
+ g.check("T7", "attribution.payout", rogueAfter.status === 1 && (await k.balance(operatorAddress)) > opBefore - parseEther("0.0005"), `the operator settled its own submission and was paid ${reward} wei to its key: income outside the cell's boundary (T8 does not count it)`);
114
+ else
115
+ g.skipped("T7", "attribution.payout", "replayed run: the operator's settlement was already made");
116
+ k.setFact("T7.unattributed", { tokenId: tokenId.toString(), submissionId: rogueId.toString(), fulfiller: operatorAddress, reward: reward.toString(), reason: "fulfiller is the operator key, not the cell account" });
117
+ // --- (4) escape attempts
118
+ const input2 = k.asf.cellBindingInput({ chainId, cell: A.address, specId: spec.specId, version: spec.version, contentHash: spec.contentHash, guardian: G, params: { profile: "foga-cell/1", cell: "A", note: "second binding presented while the first is suspended" }, authority: { kind: "owner", owner: accountRef(chainId, A.ownerAddress), guardian: accountRef(chainId, G) }, salt: "0x" + "00".repeat(31) + "02" });
119
+ const { bindingId: binding2 } = await A.adoptBinding("T7.A.adopt2", input2, 0);
120
+ k.setFact("T7.A.bindingId2", binding2);
121
+ if (!replayedStop) {
122
+ const status2 = await A.status();
123
+ g.check("T7", "escape.sibling.refused", status2.verdictName === "NotSatisfied" && status2.reasonNames.includes("StoppedByIntervener"), `with a fresh binding ${binding2.slice(0, 10)}… presented, status() = ${status2.verdictName} [${status2.reasonNames.join(",")}]: the intervener's standing stop follows the adopter and the subject`);
124
+ const stillBlocked = await g.wouldRevert(A, k.tenders.address, submitAsA);
125
+ g.check("T7", "escape.sibling.gate", stillBlocked !== null, `the commitment-class call through the fresh binding still reverts: ${stillBlocked}`);
126
+ const adopterLift = await g.wouldRevert(A, k.asf.bindingRegistryAddress, k.asf.encodeResume(bindingId, keccakJcs({ by: "adopter" })), "owner");
127
+ g.check("T7", "escape.adopter.refused", adopterLift !== null, `the adopter (owner through the account) resuming the intervener's suspension reverts: ${adopterLift}`);
128
+ }
129
+ else {
130
+ for (const n of ["escape.sibling.refused", "escape.sibling.gate", "escape.adopter.refused"])
131
+ g.skipped("T7", n, "replayed run: the suspension was already lifted");
132
+ }
133
+ // --- (5) resume, then A works through its account
134
+ await k.call("T7.G.resume", "G", k.asf.bindingRegistryAddress, k.asf.bindings().interface, "resume", [bindingId, keccakJcs({ kind: "reason", summary: "review complete; the cell may resume", step: "T7" })]);
135
+ const resumed = await A.status();
136
+ g.check("T7", "status.resumed", resumed.verdictName === "Satisfied", `status() = ${resumed.verdictName} after the guardian's resume (the standing stop is lifted with it)`);
137
+ const aBefore = await A.balance();
138
+ const submitDone = k.store.get("T7.A.settle")?.status === "confirmed";
139
+ const sub = await A.execute("T7.A.submit", k.tenders.address, submitAsA);
140
+ const subId = k.tenders.submissionIdFromReceipt(sub.receipt);
141
+ k.setFact("T7.A.submissionId", subId.toString());
142
+ await A.execute("T7.A.settle", k.tenders.address, k.tenders.encodeSettle(tokenId, subId, Buffer.from(answer, "utf8")));
143
+ const aSub = await k.tenders.submissionOf(tokenId, subId);
144
+ g.check("T7", "A.accepted", aSub.status === 1 && getAddress(aSub.fulfiller) === getAddress(A.address), `submission ${subId}: Accepted, fulfiller = the cell account`);
145
+ if (!submitDone)
146
+ g.check("T7", "A.paid", (await A.balance()) - aBefore === reward, `A's account rose by ${(await A.balance()) - aBefore} wei`);
147
+ else
148
+ g.skipped("T7", "A.paid", "replayed run: the settlement was already made");
149
+ // --- (6) the anchor gate: STALE once a liveness window passes without a heartbeat, back to ACTIVE after one
150
+ const defaultWindow = await k.aid.defaultLivenessWindow();
151
+ if (!replayedStale) {
152
+ // the anchor shortens its own window (setLivenessWindow is the anchor's call; it also counts as a heartbeat)
153
+ await A.execute("T7.A.aid.window", k.aid.address, k.aid.encodeSetLivenessWindow(LIVENESS_DEMO_WINDOW));
154
+ const window = await k.aid.livenessWindow(A.address);
155
+ const lastSeen = Number(await k.aid.lastSeen(A.address));
156
+ const now = await k.blockTimestamp();
157
+ const staleAt = (lastSeen || now) + window + 1;
158
+ if (staleAt > now)
159
+ await k.advanceTime(staleAt - now, `A's liveness window (${window} s) passes without a heartbeat`);
160
+ const stateA = await k.aid.stateName(A.address);
161
+ const staleGate = await g.wouldRevert(A, k.tenders.address, submitAsA);
162
+ g.check("T7", "anchor.stale.gate", window === LIVENESS_DEMO_WINDOW && stateA === "STALE" && staleGate !== null && /AnchorNotActive/.test(staleGate), `livenessWindow(A) = ${window} s (default ${defaultWindow} s); after it passes, state(A) = ${stateA} and a commitment-class call reverts at the gate: ${staleGate}`);
163
+ }
164
+ else {
165
+ g.skipped("T7", "anchor.stale.gate", "replayed run: the heartbeat was already sent");
166
+ }
167
+ for (const X of [A, B, C]) {
168
+ await X.execute(`T7.${X.name}.aid.heartbeat`, k.aid.address, k.aid.encodeHeartbeat());
169
+ if (k.aid.backing)
170
+ await X.execute(`T7.${X.name}.aid.backing.heartbeat`, k.aid.backing.address, k.aid.encodeHeartbeat());
171
+ }
172
+ const states = await Promise.all([A, B, C].map((X) => k.aid.stateName(X.address)));
173
+ const afterBeat = await g.wouldRevert(A, k.tenders.address, submitAsA);
174
+ // A's anchor state changes first at the identity transfer of T9 (STALE, then taken over), then at the retirement
175
+ const untilA = ["T9.A.8004.transfer", "T9.A.aid.retire"];
176
+ g.check("T7", "anchor.active.gate", states.every((s) => s === "ACTIVE") && afterBeat !== null && !/AnchorNotActive/.test(afterBeat), `states ${states.join("/")} after heartbeats; the same call now passes the gate and fails only inside the tender: ${afterBeat}`, untilA);
177
+ // the default window again: the demonstration leaves no short window behind
178
+ await A.execute("T7.A.aid.window.reset", k.aid.address, k.aid.encodeSetLivenessWindow(0));
179
+ const restored = await k.aid.livenessWindow(A.address);
180
+ g.check("T7", "anchor.window.restored", restored === defaultWindow, `livenessWindow(A) = ${restored} s after the reset (the registry's default)`, "T9.A.aid.retire");
181
+ if (k.aid.backing) {
182
+ const backingStates = await Promise.all([A, B, C].map((X) => k.aid.stateName(X.address, k.aid.backing.address)));
183
+ g.check("T7", "anchor.backing.active", backingStates.every((s) => s === "ACTIVE"), `states ${backingStates.join("/")} in the backing registry ${k.aid.backing.address} after heartbeats`, untilA);
184
+ }
185
+ k.evidence("T7", "safety", {
186
+ binding: bindingId,
187
+ binding2,
188
+ tender: { tokenId: tokenId.toString(), completions: 2, reward: reward.toString() },
189
+ suspension: { by: G, restrictions: Restriction.noNewRisk | Restriction.deriskOnly, response, reason },
190
+ kernelDecision: k.fact("T7.kernel.decision") ?? null,
191
+ bypass: k.fact("T7.bypass") ?? null,
192
+ derisk: { action: "ADRFServiceEscrow.withdrawBalance", amount: earned.toString() },
193
+ unattributed: k.fact("T7.unattributed"),
194
+ attributed: { submissionId: subId.toString(), fulfiller: A.address },
195
+ liveness: { demonstrationWindow: LIVENESS_DEMO_WINDOW, defaultWindow, backingRegistry: k.aid.backing?.address ?? null },
196
+ txs: { suspend: k.store.get("T7.G.suspend")?.txHash, resume: k.store.get("T7.G.resume")?.txHash, operatorSubmit: rogue.txHash, submit: sub.txHash, window: k.store.get("T7.A.aid.window")?.txHash, heartbeats: [A, B, C].map((X) => k.store.get(`T7.${X.name}.aid.heartbeat`)?.txHash), windowReset: k.store.get("T7.A.aid.window.reset")?.txHash },
197
+ });
198
+ }