@jinn-network/task-execution-protocol 0.0.0 → 0.1.0-canary.sha.05ab5a98a8b30392ca448de12748b19b0c947684

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 (87) hide show
  1. package/README.md +36 -1
  2. package/dist/canonical.d.ts +3 -0
  3. package/dist/canonical.js +24 -0
  4. package/dist/descriptors.d.ts +26 -0
  5. package/dist/descriptors.js +6 -0
  6. package/dist/errors.d.ts +4 -0
  7. package/dist/errors.js +23 -0
  8. package/dist/events.d.ts +10 -0
  9. package/dist/events.js +18 -0
  10. package/dist/fixtures.d.ts +31 -0
  11. package/dist/fixtures.js +45 -0
  12. package/dist/fold.d.ts +46 -0
  13. package/dist/fold.js +160 -0
  14. package/dist/hashing.d.ts +2 -0
  15. package/dist/hashing.js +8 -0
  16. package/dist/identifiers.d.ts +9 -0
  17. package/dist/identifiers.js +39 -0
  18. package/dist/index.d.ts +35 -0
  19. package/dist/index.js +33 -0
  20. package/dist/json.d.ts +23 -0
  21. package/dist/json.js +135 -0
  22. package/dist/order.d.ts +10 -0
  23. package/dist/order.js +17 -0
  24. package/dist/requirements.d.ts +22 -0
  25. package/dist/requirements.js +148 -0
  26. package/dist/schemas/common.d.ts +75 -0
  27. package/dist/schemas/common.js +76 -0
  28. package/dist/schemas/delivery.d.ts +36 -0
  29. package/dist/schemas/delivery.js +29 -0
  30. package/dist/schemas/dispatch-context.d.ts +13 -0
  31. package/dist/schemas/dispatch-context.js +15 -0
  32. package/dist/schemas/observation.d.ts +135 -0
  33. package/dist/schemas/observation.js +145 -0
  34. package/dist/schemas/submission.d.ts +29 -0
  35. package/dist/schemas/submission.js +34 -0
  36. package/dist/schemas/task.d.ts +58 -0
  37. package/dist/schemas/task.js +45 -0
  38. package/dist/sealing.d.ts +16 -0
  39. package/dist/sealing.js +45 -0
  40. package/dist/states.d.ts +7 -0
  41. package/dist/states.js +7 -0
  42. package/dist/validators.d.ts +13 -0
  43. package/dist/validators.js +32 -0
  44. package/fixtures/adversarial-v1/contradictory-terminals/observations.json +31 -0
  45. package/fixtures/adversarial-v1/digest-mismatch/claimed-digest.json +4 -0
  46. package/fixtures/adversarial-v1/digest-mismatch/document.json +4 -0
  47. package/fixtures/adversarial-v1/dispatch-context-grafting/NOTE.md +1 -0
  48. package/fixtures/adversarial-v1/dispatch-context-grafting/grafted-dispatch-context.json +6 -0
  49. package/fixtures/adversarial-v1/expired-then-late-terminal/fold-options.json +4 -0
  50. package/fixtures/adversarial-v1/expired-then-late-terminal/log-with-terminal.json +37 -0
  51. package/fixtures/adversarial-v1/expired-then-late-terminal/log-without-terminal.json +23 -0
  52. package/fixtures/adversarial-v1/extension-override/task.json +18 -0
  53. package/fixtures/adversarial-v1/forged-cross-attempt-supersedes/delivery-a.json +1 -0
  54. package/fixtures/adversarial-v1/forged-cross-attempt-supersedes/delivery-b-forged-supersedes.json +1 -0
  55. package/fixtures/adversarial-v1/idempotency-scopes/submission-a.json +1 -0
  56. package/fixtures/adversarial-v1/idempotency-scopes/submission-b-same-key-different-bytes.json +1 -0
  57. package/fixtures/adversarial-v1/idempotency-scopes/submission-c-different-requester-same-key.json +1 -0
  58. package/fixtures/adversarial-v1/illegal-terminal-transition/observations.json +30 -0
  59. package/fixtures/adversarial-v1/leaked-task-resubmission/confidential-task.json +1 -0
  60. package/fixtures/adversarial-v1/leaked-task-resubmission/unauthorized-submission.json +1 -0
  61. package/fixtures/adversarial-v1/lost-then-corrected/observations.json +30 -0
  62. package/fixtures/adversarial-v1/malformed-document/task.json +10 -0
  63. package/fixtures/adversarial-v1/manifest.json +75 -0
  64. package/fixtures/adversarial-v1/oversized-content/oversized-observation.json +14 -0
  65. package/fixtures/adversarial-v1/oversized-content/resource-descriptor.json +8 -0
  66. package/fixtures/adversarial-v1/replayed-and-out-of-order/observations.json +51 -0
  67. package/fixtures/adversarial-v1/sequence-boundary/observations.json +8 -0
  68. package/fixtures/golden-task-execution-v1/artifacts/patch.diff +6 -0
  69. package/fixtures/golden-task-execution-v1/conformance-report.json +23 -0
  70. package/fixtures/golden-task-execution-v1/equivalence/expected-digest.json +3 -0
  71. package/fixtures/golden-task-execution-v1/equivalence/task-a.json +1 -0
  72. package/fixtures/golden-task-execution-v1/equivalence/task-b.json +1 -0
  73. package/fixtures/golden-task-execution-v1/local/delivery.json +1 -0
  74. package/fixtures/golden-task-execution-v1/local/observations.json +70 -0
  75. package/fixtures/golden-task-execution-v1/local/submission.json +1 -0
  76. package/fixtures/golden-task-execution-v1/marketplace/delivery.json +1 -0
  77. package/fixtures/golden-task-execution-v1/marketplace/observations.json +70 -0
  78. package/fixtures/golden-task-execution-v1/marketplace/submission.json +1 -0
  79. package/fixtures/golden-task-execution-v1/task.json +1 -0
  80. package/fixtures/manifest.sha256.json +150 -0
  81. package/package.json +49 -5
  82. package/profiles/task-execution/v1/profile.json +15 -0
  83. package/schemas/delivery.schema.json +139 -0
  84. package/schemas/dispatch-context.schema.json +28 -0
  85. package/schemas/observation.schema.json +526 -0
  86. package/schemas/submission.schema.json +131 -0
  87. package/schemas/task.schema.json +213 -0
package/README.md CHANGED
@@ -1 +1,36 @@
1
- Placeholder. Real versions come from stack-npm-publish.yml.
1
+ # @jinn-network/task-execution-protocol
2
+
3
+ Pure, I/O-free reference implementation of the Jinn Task Execution Protocol (TEP) v1: types,
4
+ JSON Schemas, sealing + digest functions, the observation fold, family validators, and
5
+ deterministic Attempt-URI derivation. No I/O, no Jinn dependencies — Evidence Protocol
6
+ references are structural `{ family, digest }` fields, not a package dependency.
7
+
8
+ See the design: `docs/superpowers/specs/2026-07-27-task-execution-protocol-and-stack-design.md`.
9
+ Carried-amendment implementation notes: `docs/superpowers/specs/2026-07-28-tep-v1-implementation-addendum.md`.
10
+
11
+ ## The frozen public surface
12
+
13
+ Records: `TaskSpecification`, `SubmissionRecord`, `DeliveryRecord`, `DispatchContext`,
14
+ `ProtocolObservation`, `ResourceDescriptor`, `EvidenceRecordReference`, `AttemptDescriptor`
15
+ (a projection materialized by a backend's `observe()`, §9/§22).
16
+
17
+ Pure functions: `sealTask` / `sealSubmission` / `sealDelivery` (JCS + I-JSON enforcement),
18
+ `documentDigest`, `validateTask` / `validateSubmission` / `validateDelivery` /
19
+ `validateDispatchContext` / `validateObservation`, `foldObservations` (§10.4),
20
+ `deriveAttemptUri` (§9.2 UUIDv5, exports the frozen `TEP_ATTEMPT_NAMESPACE`), and
21
+ `mergeRequirements` (the profiles §5.1 tighten-only run-pinning merge, carried amendment 1).
22
+
23
+ ## The seal-once rule (§6.1)
24
+
25
+ Every sealed document family (Task, Submission, Delivery) canonicalizes exactly once with
26
+ RFC 8785 JCS under I-JSON at sealing. Those bytes are the document forever — no consumer ever
27
+ re-canonicalizes to check a digest. `documentDigest(bytes)` always hashes the exact bytes
28
+ received, never a re-derived form.
29
+
30
+ ## Fixtures
31
+
32
+ `fixtures/golden-task-execution-v1/` — a complete local-and-marketplace scenario pair over one
33
+ Task digest, plus a key-order-sensitive equivalence record (`equivalence/`). `fixtures/adversarial-v1/`
34
+ — the §24 adversarial minimum set; this package ships the fixture bytes plus a `manifest.json`
35
+ describing each case's expected disposition. `@jinn-network/task-execution-testing` (a sibling
36
+ package, not built by this milestone) drives the conformance assertions against them.
@@ -0,0 +1,3 @@
1
+ import { type JsonValue } from "./json.js";
2
+ /** RFC 8785 JCS over the I-JSON-integer subset; those bytes are the document forever (§6.1). */
3
+ export declare function serializeCanonicalJson(value: JsonValue): Uint8Array;
@@ -0,0 +1,24 @@
1
+ import { compareCodeUnitStrings } from "./order.js";
2
+ import { cloneIJsonValue } from "./json.js";
3
+ const encoder = new TextEncoder();
4
+ /** Emit RFC 8785 JCS by explicit sorted-key iteration — insertion order is never trusted (§7.14). */
5
+ function serialize(value) {
6
+ if (value === null)
7
+ return "null";
8
+ if (typeof value === "boolean")
9
+ return value ? "true" : "false";
10
+ if (typeof value === "number")
11
+ return String(value);
12
+ if (typeof value === "string")
13
+ return JSON.stringify(value); // JCS string escaping (I-JSON subset)
14
+ if (Array.isArray(value)) {
15
+ return `[${value.map((element) => serialize(element)).join(",")}]`;
16
+ }
17
+ const keys = Object.keys(value)
18
+ .sort(compareCodeUnitStrings); // sorted array drives emission
19
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(value[k])}`).join(",")}}`;
20
+ }
21
+ /** RFC 8785 JCS over the I-JSON-integer subset; those bytes are the document forever (§6.1). */
22
+ export function serializeCanonicalJson(value) {
23
+ return encoder.encode(serialize(cloneIJsonValue(value)));
24
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * in-toto v1 ResourceDescriptor (§6.4). Digest is identity; IPFS CIDs, gateway URLs, and git
3
+ * object identifiers are locator hints riding in uri/downloadLocation/annotations — never
4
+ * identity. Bounded inline `content` is permitted for small values and always accompanied by
5
+ * its digest.
6
+ */
7
+ export interface ResourceDescriptor {
8
+ name?: string;
9
+ uri?: string;
10
+ digest?: Record<string, string>;
11
+ mediaType?: string;
12
+ downloadLocation?: string;
13
+ annotations?: Record<string, unknown>;
14
+ content?: string;
15
+ }
16
+ /** Structural check: a ResourceDescriptor must carry at least one of uri/digest/content (§6.4). */
17
+ export declare function resourceDescriptorHasLocator(descriptor: ResourceDescriptor): boolean;
18
+ /**
19
+ * The structural Evidence Protocol seam (§6.4): a reference by family + digest only. This is a
20
+ * structural convention, not a package dependency — no import of any evidence package.
21
+ */
22
+ export type EvidenceRecordFamily = "execution-evidence" | "result-evaluation" | "execution-verification";
23
+ export interface EvidenceRecordReference {
24
+ family: EvidenceRecordFamily;
25
+ digest: `sha256:${string}`;
26
+ }
@@ -0,0 +1,6 @@
1
+ /** Structural check: a ResourceDescriptor must carry at least one of uri/digest/content (§6.4). */
2
+ export function resourceDescriptorHasLocator(descriptor) {
3
+ return descriptor.uri !== undefined
4
+ || (descriptor.digest !== undefined && Object.keys(descriptor.digest).length > 0)
5
+ || descriptor.content !== undefined;
6
+ }
@@ -0,0 +1,4 @@
1
+ export declare const TASK_EXECUTION_ERROR_CATEGORIES: readonly ["invalid-document", "unsupported-profile", "unsupported-requirement", "unsupported-capability", "invalid-reference", "content-corruption", "access-denied", "submission-conflict", "attempt-not-found", "dependency-unavailable", "backend-unavailable", "operation-aborted", "deadline-exceeded", "transport-failure", "result-unavailable", "protocol-violation"];
2
+ export type TaskExecutionErrorCategory = (typeof TASK_EXECUTION_ERROR_CATEGORIES)[number];
3
+ export declare const ERROR_RETRYABLE: Record<TaskExecutionErrorCategory, boolean>;
4
+ export declare function isRetryable(category: TaskExecutionErrorCategory): boolean;
package/dist/errors.js ADDED
@@ -0,0 +1,23 @@
1
+ export const TASK_EXECUTION_ERROR_CATEGORIES = [
2
+ "invalid-document", "unsupported-profile", "unsupported-requirement",
3
+ "unsupported-capability", "invalid-reference", "content-corruption",
4
+ "access-denied", "submission-conflict", "attempt-not-found",
5
+ "dependency-unavailable", "backend-unavailable", "operation-aborted",
6
+ "deadline-exceeded", "transport-failure", "result-unavailable",
7
+ "protocol-violation",
8
+ ];
9
+ // retryable defaults (§13 says each category carries a machine-readable flag;
10
+ // the flags themselves are a field-level refinement — pinned by errors.test.ts).
11
+ export const ERROR_RETRYABLE = {
12
+ "invalid-document": false, "unsupported-profile": false,
13
+ "unsupported-requirement": false, "unsupported-capability": false,
14
+ "invalid-reference": false, "content-corruption": false,
15
+ "access-denied": false, "submission-conflict": false,
16
+ "attempt-not-found": false, "dependency-unavailable": true,
17
+ "backend-unavailable": true, "operation-aborted": true,
18
+ "deadline-exceeded": true, "transport-failure": true,
19
+ "result-unavailable": false, "protocol-violation": false,
20
+ };
21
+ export function isRetryable(category) {
22
+ return ERROR_RETRYABLE[category];
23
+ }
@@ -0,0 +1,10 @@
1
+ export declare const SUBMISSION_EVENT_TYPES: readonly ["network.jinn.task-execution.submission-accepted.v1", "network.jinn.task-execution.submission-rejected.v1", "network.jinn.task-execution.submission-closed.v1"];
2
+ export declare const ATTEMPT_EVENT_TYPES: readonly ["network.jinn.task-execution.attempt-engaged.v1", "network.jinn.task-execution.attempt-started.v1", "network.jinn.task-execution.progress.v1", "network.jinn.task-execution.cancel-requested.v1", "network.jinn.task-execution.cancel-acknowledged.v1", "network.jinn.task-execution.execution-observed.v1", "network.jinn.task-execution.delivery-recorded.v1", "network.jinn.task-execution.attempt-terminal.v1"];
3
+ export declare const TASK_EXECUTION_EVENT_TYPES: readonly ["network.jinn.task-execution.submission-accepted.v1", "network.jinn.task-execution.submission-rejected.v1", "network.jinn.task-execution.submission-closed.v1", "network.jinn.task-execution.attempt-engaged.v1", "network.jinn.task-execution.attempt-started.v1", "network.jinn.task-execution.progress.v1", "network.jinn.task-execution.cancel-requested.v1", "network.jinn.task-execution.cancel-acknowledged.v1", "network.jinn.task-execution.execution-observed.v1", "network.jinn.task-execution.delivery-recorded.v1", "network.jinn.task-execution.attempt-terminal.v1"];
4
+ /** Highest representable value under the fixed-width 16-digit `sequence` encoding (§10.1). */
5
+ export declare const MAX_SEQUENCE = 9999999999999999n;
6
+ /**
7
+ * Fixed-width, zero-padded 16-digit decimal encoding of the CloudEvents `sequence` extension
8
+ * (§10.1) — orders lexicographically, which is what CloudEvents' `sequence` mandates.
9
+ */
10
+ export declare function formatSequence(n: bigint): string;
package/dist/events.js ADDED
@@ -0,0 +1,18 @@
1
+ // The reverse-DNS type strings are the single source of truth for the discriminated observation
2
+ // payload schema (schemas/observation.ts) and are re-exported here under the frozen §22 names.
3
+ import { ATTEMPT_OBSERVATION_TYPES, OBSERVATION_TYPES, SUBMISSION_OBSERVATION_TYPES, } from "./schemas/observation.js";
4
+ export const SUBMISSION_EVENT_TYPES = SUBMISSION_OBSERVATION_TYPES;
5
+ export const ATTEMPT_EVENT_TYPES = ATTEMPT_OBSERVATION_TYPES;
6
+ export const TASK_EXECUTION_EVENT_TYPES = OBSERVATION_TYPES;
7
+ /** Highest representable value under the fixed-width 16-digit `sequence` encoding (§10.1). */
8
+ export const MAX_SEQUENCE = 9999999999999999n;
9
+ /**
10
+ * Fixed-width, zero-padded 16-digit decimal encoding of the CloudEvents `sequence` extension
11
+ * (§10.1) — orders lexicographically, which is what CloudEvents' `sequence` mandates.
12
+ */
13
+ export function formatSequence(n) {
14
+ if (n < 0n || n > MAX_SEQUENCE) {
15
+ throw new RangeError(`sequence out of the 16-digit range: ${n}`);
16
+ }
17
+ return n.toString().padStart(16, "0");
18
+ }
@@ -0,0 +1,31 @@
1
+ /** The golden scenario pair's shared Task, sealed bytes (§24). */
2
+ export declare function loadGoldenTaskBytes(): Promise<Uint8Array>;
3
+ export type GoldenScenario = "local" | "marketplace";
4
+ export declare function loadGoldenSubmissionBytes(scenario: GoldenScenario): Promise<Uint8Array>;
5
+ export declare function loadGoldenDeliveryBytes(scenario: GoldenScenario): Promise<Uint8Array>;
6
+ export declare function loadGoldenObservations(scenario: GoldenScenario): Promise<unknown[]>;
7
+ export interface GoldenConformanceReport {
8
+ taskDigest: `sha256:${string}`;
9
+ scenarios: Record<GoldenScenario, {
10
+ submission: string;
11
+ submissionDigest: `sha256:${string}`;
12
+ attempt: string;
13
+ deliveryDigest: `sha256:${string}`;
14
+ }>;
15
+ }
16
+ export declare function loadGoldenConformanceReport(): Promise<GoldenConformanceReport>;
17
+ export declare function loadEquivalenceTaskBytes(variant: "a" | "b"): Promise<Uint8Array>;
18
+ export declare function loadEquivalenceExpectedDigest(): Promise<`sha256:${string}`>;
19
+ export interface AdversarialManifestEntry {
20
+ id: string;
21
+ description: string;
22
+ expectedDisposition: string;
23
+ }
24
+ export interface AdversarialManifest {
25
+ taskDigest: `sha256:${string}`;
26
+ fixtures: readonly AdversarialManifestEntry[];
27
+ }
28
+ export declare function loadAdversarialManifest(): Promise<AdversarialManifest>;
29
+ /** Reads an arbitrary file inside a named adversarial fixture directory. */
30
+ export declare function readAdversarialFile(id: string, filename: string): Promise<Uint8Array>;
31
+ export declare function readAdversarialJson(id: string, filename: string): Promise<unknown>;
@@ -0,0 +1,45 @@
1
+ import { readFile } from "node:fs/promises";
2
+ function fixtureUrl(relativePath) {
3
+ return new URL(`../fixtures/${relativePath}`, import.meta.url);
4
+ }
5
+ async function readFixtureBytes(relativePath) {
6
+ return new Uint8Array(await readFile(fixtureUrl(relativePath)));
7
+ }
8
+ async function readFixtureJson(relativePath) {
9
+ return JSON.parse(await readFile(fixtureUrl(relativePath), "utf8"));
10
+ }
11
+ const GOLDEN_ROOT = "golden-task-execution-v1";
12
+ /** The golden scenario pair's shared Task, sealed bytes (§24). */
13
+ export async function loadGoldenTaskBytes() {
14
+ return readFixtureBytes(`${GOLDEN_ROOT}/task.json`);
15
+ }
16
+ export async function loadGoldenSubmissionBytes(scenario) {
17
+ return readFixtureBytes(`${GOLDEN_ROOT}/${scenario}/submission.json`);
18
+ }
19
+ export async function loadGoldenDeliveryBytes(scenario) {
20
+ return readFixtureBytes(`${GOLDEN_ROOT}/${scenario}/delivery.json`);
21
+ }
22
+ export async function loadGoldenObservations(scenario) {
23
+ return (await readFixtureJson(`${GOLDEN_ROOT}/${scenario}/observations.json`));
24
+ }
25
+ export async function loadGoldenConformanceReport() {
26
+ return (await readFixtureJson(`${GOLDEN_ROOT}/conformance-report.json`));
27
+ }
28
+ export async function loadEquivalenceTaskBytes(variant) {
29
+ return readFixtureBytes(`${GOLDEN_ROOT}/equivalence/task-${variant}.json`);
30
+ }
31
+ export async function loadEquivalenceExpectedDigest() {
32
+ const { digest } = (await readFixtureJson(`${GOLDEN_ROOT}/equivalence/expected-digest.json`));
33
+ return digest;
34
+ }
35
+ const ADVERSARIAL_ROOT = "adversarial-v1";
36
+ export async function loadAdversarialManifest() {
37
+ return (await readFixtureJson(`${ADVERSARIAL_ROOT}/manifest.json`));
38
+ }
39
+ /** Reads an arbitrary file inside a named adversarial fixture directory. */
40
+ export async function readAdversarialFile(id, filename) {
41
+ return readFixtureBytes(`${ADVERSARIAL_ROOT}/${id}/${filename}`);
42
+ }
43
+ export async function readAdversarialJson(id, filename) {
44
+ return readFixtureJson(`${ADVERSARIAL_ROOT}/${id}/${filename}`);
45
+ }
package/dist/fold.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { ProtocolObservation } from "./schemas/observation.js";
2
+ import type { AttemptState, FailureBlame } from "./states.js";
3
+ /** Pure projection over an Attempt's observation log (§9/§10.4/§22). */
4
+ export interface DerivedAttemptState {
5
+ state: AttemptState;
6
+ terminal: boolean;
7
+ contradictory: boolean;
8
+ cancelRequested: boolean;
9
+ blame?: FailureBlame;
10
+ executor?: string;
11
+ effectiveDeadline?: string;
12
+ executionIds: readonly string[];
13
+ deliveries: readonly {
14
+ digest: `sha256:${string}`;
15
+ }[];
16
+ }
17
+ /**
18
+ * The §9/§22 Attempt projection, materialized at `observe()`'s surface (backend, Task 2.x;
19
+ * program §7.16). Defined here so the frozen §22 name is a real export; its reference fields
20
+ * (`task`/`submission`/`annotations`) are supplied at materialization time by the consumer that
21
+ * holds the Submission + dispatch context — the pure log fold below does not carry them.
22
+ */
23
+ export interface AttemptDescriptor {
24
+ attempt: `urn:uuid:${string}`;
25
+ task: `sha256:${string}`;
26
+ submission: `urn:uuid:${string}`;
27
+ annotations?: Readonly<Record<string, unknown>>;
28
+ derived: DerivedAttemptState;
29
+ }
30
+ export interface FoldObservationsOptions {
31
+ /** The clock reading to derive provisional `expired` against (§10.4 rule 5). */
32
+ now?: string;
33
+ /** The Attempt's effective absolute deadline, from the engaging Submission. */
34
+ effectiveDeadline?: string;
35
+ }
36
+ /**
37
+ * Implements §10.4 exactly. Without `opts`, this is a pure log fold — no provisional `expired`.
38
+ *
39
+ * SELF-FILTER (§10.1/§10.4): the caller MAY hand this function a multi-source log (e.g. every
40
+ * observation recorded for an Attempt, not pre-filtered). Once the first `attempt-engaged` in
41
+ * sequence order pins the authoritative source, every observation whose envelope `source` does
42
+ * not match it — including a forged or merely corroborating observation from a different
43
+ * producer — is excluded from the fold entirely: it neither updates derived state nor
44
+ * participates in terminal resolution (so it can never raise a false `contradictory`).
45
+ */
46
+ export declare function foldObservations(observations: readonly ProtocolObservation[], opts?: FoldObservationsOptions): DerivedAttemptState;
package/dist/fold.js ADDED
@@ -0,0 +1,160 @@
1
+ function isTerminal(observation) {
2
+ return observation.type === "network.jinn.task-execution.attempt-terminal.v1";
3
+ }
4
+ function dedupe(observations) {
5
+ const seen = new Set();
6
+ const result = [];
7
+ for (const observation of observations) {
8
+ // A raw byte (not an escape) was used as the delimiter here pre-fix -- invisible in a
9
+ // normal read, and (like the fake backend's idempotency scope key, see
10
+ // testing/src/fake-backend.ts) it made this file register as binary to git, hiding this
11
+ // function's diffs from review. Named and escaped explicitly now; same delimiter role
12
+ // (source/id are unconstrained strings, so an undelimited or ambiguous join could
13
+ // collide two distinct pairs into one dedupe key), same U+001F convention this tree
14
+ // already uses in `deriveAttemptUri` and the fake backend's idempotency scope.
15
+ const key = `${observation.source}\u001f${observation.id}`;
16
+ if (seen.has(key))
17
+ continue;
18
+ seen.add(key);
19
+ result.push(observation);
20
+ }
21
+ return result;
22
+ }
23
+ /**
24
+ * Resolves the authoritative terminal from the sequence-ordered list of `attempt-terminal`
25
+ * observations (§10.4 rules 4 and 6). A single terminal is authoritative outright. Multiple
26
+ * terminals resolve by walking in order: while the currently accepted terminal is the
27
+ * quasi-terminal `lost`, a later terminal supersedes it without contradiction (rule 6); any
28
+ * other terminal-to-terminal transition is contradictory, and the derived state is the first
29
+ * terminal in sequence order (rule 4).
30
+ */
31
+ function resolveTerminal(terminals) {
32
+ if (terminals.length === 0)
33
+ return undefined;
34
+ let accepted = terminals[0];
35
+ let contradictory = false;
36
+ for (let i = 1; i < terminals.length; i++) {
37
+ if (!contradictory && accepted.data.state === "lost") {
38
+ accepted = terminals[i]; // rule 6: quasi-terminal supersession, no contradiction
39
+ }
40
+ else {
41
+ contradictory = true;
42
+ accepted = terminals[0]; // rule 4: contradictory falls back to the first-in-sequence terminal
43
+ }
44
+ }
45
+ return { accepted, contradictory };
46
+ }
47
+ /**
48
+ * §10.1/§10.4: `attempt-engaged` pins the authoritative observation-producer source (its
49
+ * `data.source` field) for the rest of the Attempt's log. If multiple `attempt-engaged`
50
+ * observations from different sources appear (a race, or a forgery), the first one in sequence
51
+ * order wins as the engagement — every other observation, including a later `attempt-engaged`
52
+ * from a different source, is non-authoritative.
53
+ */
54
+ function findAuthoritativeSource(ordered) {
55
+ for (const observation of ordered) {
56
+ if (observation.type === "network.jinn.task-execution.attempt-engaged.v1") {
57
+ return observation.data.source;
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Implements §10.4 exactly. Without `opts`, this is a pure log fold — no provisional `expired`.
64
+ *
65
+ * SELF-FILTER (§10.1/§10.4): the caller MAY hand this function a multi-source log (e.g. every
66
+ * observation recorded for an Attempt, not pre-filtered). Once the first `attempt-engaged` in
67
+ * sequence order pins the authoritative source, every observation whose envelope `source` does
68
+ * not match it — including a forged or merely corroborating observation from a different
69
+ * producer — is excluded from the fold entirely: it neither updates derived state nor
70
+ * participates in terminal resolution (so it can never raise a false `contradictory`).
71
+ */
72
+ export function foldObservations(observations, opts) {
73
+ const ordered = dedupe(observations)
74
+ .slice()
75
+ .sort((a, b) => (a.sequence < b.sequence ? -1 : a.sequence > b.sequence ? 1 : 0));
76
+ const authoritativeSource = findAuthoritativeSource(ordered);
77
+ let running = false;
78
+ let cancelRequested = false;
79
+ let executor;
80
+ let effectiveDeadline;
81
+ const executionIds = [];
82
+ const deliveries = [];
83
+ const terminals = [];
84
+ for (const observation of ordered) {
85
+ if (authoritativeSource !== undefined && observation.source !== authoritativeSource)
86
+ continue;
87
+ switch (observation.type) {
88
+ case "network.jinn.task-execution.attempt-engaged.v1": {
89
+ if (observation.data.executor !== undefined)
90
+ executor = observation.data.executor;
91
+ effectiveDeadline = observation.data.effectiveDeadline;
92
+ break;
93
+ }
94
+ case "network.jinn.task-execution.attempt-started.v1": {
95
+ running = true;
96
+ if (observation.data.executor !== undefined)
97
+ executor = observation.data.executor;
98
+ break;
99
+ }
100
+ case "network.jinn.task-execution.cancel-requested.v1": {
101
+ cancelRequested = true;
102
+ break;
103
+ }
104
+ case "network.jinn.task-execution.execution-observed.v1": {
105
+ executionIds.push(observation.data.executionId);
106
+ break;
107
+ }
108
+ case "network.jinn.task-execution.delivery-recorded.v1": {
109
+ // The Sha256Digest zod schema enforces the `sha256:<hex>` shape at runtime; regex-based
110
+ // zod schemas infer as `string`, so the narrower template type is asserted here.
111
+ deliveries.push({ digest: observation.data.digest });
112
+ break;
113
+ }
114
+ default:
115
+ break;
116
+ }
117
+ if (isTerminal(observation))
118
+ terminals.push(observation);
119
+ }
120
+ const resolved = resolveTerminal(terminals);
121
+ if (resolved !== undefined) {
122
+ return {
123
+ state: resolved.accepted.data.state,
124
+ terminal: true,
125
+ contradictory: resolved.contradictory,
126
+ cancelRequested,
127
+ blame: resolved.accepted.data.blame,
128
+ executor,
129
+ effectiveDeadline,
130
+ executionIds,
131
+ deliveries,
132
+ };
133
+ }
134
+ // No authoritative terminal in the log. Provisional `expired` derives only when the caller
135
+ // supplies both a clock reading and the effective deadline (program §7.16 / §10.4 rule 5).
136
+ if (opts?.now !== undefined
137
+ && opts.effectiveDeadline !== undefined
138
+ && new Date(opts.now).getTime() > new Date(opts.effectiveDeadline).getTime()) {
139
+ return {
140
+ state: "expired",
141
+ terminal: true,
142
+ contradictory: false,
143
+ cancelRequested,
144
+ executor,
145
+ effectiveDeadline,
146
+ executionIds,
147
+ deliveries,
148
+ };
149
+ }
150
+ return {
151
+ state: running ? "running" : "pending",
152
+ terminal: false,
153
+ contradictory: false,
154
+ cancelRequested,
155
+ executor,
156
+ effectiveDeadline,
157
+ executionIds,
158
+ deliveries,
159
+ };
160
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sha256Hex(bytes: Uint8Array): string;
2
+ export declare function documentDigest(bytes: Uint8Array): `sha256:${string}`;
@@ -0,0 +1,8 @@
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+ import { bytesToHex } from "@noble/hashes/utils.js";
3
+ export function sha256Hex(bytes) {
4
+ return bytesToHex(sha256(bytes));
5
+ }
6
+ export function documentDigest(bytes) {
7
+ return `sha256:${sha256Hex(bytes)}`;
8
+ }
@@ -0,0 +1,9 @@
1
+ import { compareCodeUnitStrings } from "./order.js";
2
+ /** Exact wire media types for the three sealed Task Execution Protocol records. */
3
+ export declare const TASK_MEDIA_TYPE: "application/vnd.jinn.task-execution.task.v1+json";
4
+ export declare const SUBMISSION_MEDIA_TYPE: "application/vnd.jinn.task-execution.submission.v1+json";
5
+ export declare const DELIVERY_MEDIA_TYPE: "application/vnd.jinn.task-execution.delivery.v1+json";
6
+ export declare const TEP_ATTEMPT_NAMESPACE = "158601b2-0cad-5f8b-89f2-c4a36f79fc78";
7
+ export declare function isValidUrnUuid(uri: string): boolean;
8
+ export declare function deriveAttemptUri(bindingName: string, correlationTuple: readonly (string | number)[]): `urn:uuid:${string}`;
9
+ export { compareCodeUnitStrings };
@@ -0,0 +1,39 @@
1
+ import { sha1 } from "@noble/hashes/legacy.js"; // @noble/hashes v2 exposes sha1 here; verified at install
2
+ import { compareCodeUnitStrings } from "./order.js";
3
+ /** Exact wire media types for the three sealed Task Execution Protocol records. */
4
+ export const TASK_MEDIA_TYPE = "application/vnd.jinn.task-execution.task.v1+json";
5
+ export const SUBMISSION_MEDIA_TYPE = "application/vnd.jinn.task-execution.submission.v1+json";
6
+ export const DELIVERY_MEDIA_TYPE = "application/vnd.jinn.task-execution.delivery.v1+json";
7
+ // Fixed TEP Attempt-URI namespace: a v5 UUID over the RFC 4122 "URL" namespace
8
+ // (6ba7b811-9dad-11d1-80b4-00c04fd430c8) and the name "jinn.network/task-execution/attempt".
9
+ // EXPORTED (program §7.2): the marketplace binding consumes this constant; it never re-derives
10
+ // its own. Computed once and frozen; pinned by identifiers.test.ts.
11
+ export const TEP_ATTEMPT_NAMESPACE = "158601b2-0cad-5f8b-89f2-c4a36f79fc78";
12
+ // Frozen name-construction delimiter (program §7.2): the ASCII unit separator (U+001F), a
13
+ // control char that cannot appear in the parts, so variable-length parts never concatenate
14
+ // ambiguously.
15
+ const NAME_UNIT_SEPARATOR = "\u001f";
16
+ const UUID_RE = /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
+ export function isValidUrnUuid(uri) {
18
+ return UUID_RE.test(uri);
19
+ }
20
+ function uuidBytes(uuid) {
21
+ const hex = uuid.replaceAll("-", "");
22
+ return Uint8Array.from({ length: 16 }, (_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16));
23
+ }
24
+ function formatUuid(b) {
25
+ const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
26
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
27
+ }
28
+ export function deriveAttemptUri(bindingName, correlationTuple) {
29
+ // Frozen deterministic name (§7.2): bindingName + tuple parts, unit-separator-delimited so
30
+ // variable-length parts never concatenate ambiguously.
31
+ const name = [bindingName, ...correlationTuple.map(String)].join(NAME_UNIT_SEPARATOR);
32
+ const input = new Uint8Array([...uuidBytes(TEP_ATTEMPT_NAMESPACE), ...new TextEncoder().encode(name)]);
33
+ const hash = sha1(input).slice(0, 16);
34
+ hash[6] = (hash[6] & 0x0f) | 0x50; // version 5
35
+ hash[8] = (hash[8] & 0x3f) | 0x80; // variant RFC 4122
36
+ return `urn:uuid:${formatUuid(hash)}`;
37
+ }
38
+ // compareCodeUnitStrings re-exported for callers assembling deterministic tuples.
39
+ export { compareCodeUnitStrings };
@@ -0,0 +1,35 @@
1
+ export declare const TASK_EXECUTION_PROTOCOL_URI = "https://spec.jinn.network/profiles/task-execution/v1";
2
+ export { compareCodeUnitStrings } from "./order.js";
3
+ export { documentDigest, sha256Hex } from "./hashing.js";
4
+ export { serializeCanonicalJson } from "./canonical.js";
5
+ export { IJsonNumberError, UndefinedArrayElementError, assertIJsonInteger } from "./json.js";
6
+ export type { JsonValue } from "./json.js";
7
+ export { resourceDescriptorHasLocator } from "./descriptors.js";
8
+ export type { EvidenceRecordFamily, EvidenceRecordReference, ResourceDescriptor } from "./descriptors.js";
9
+ export { DELIVERY_MEDIA_TYPE, SUBMISSION_MEDIA_TYPE, TASK_MEDIA_TYPE, TEP_ATTEMPT_NAMESPACE, deriveAttemptUri, isValidUrnUuid, } from "./identifiers.js";
10
+ export { DigestMapSchema, EFFORT_TIERS, EVIDENCE_RECORD_FAMILIES, EffortTierSchema, EvidenceRecordReferenceSchema, HarnessPinSchema, IsolationPolicySchema, LoadoutSchema, ModelPinSchema, PreferencesMap, RequirementsMap, ResourceDescriptorSchema, Rfc3339, Sha256Digest, UrnUuid, } from "./schemas/common.js";
11
+ export type { EffortTier } from "./schemas/common.js";
12
+ export { TaskOutputSlotSchema, TaskSpecificationSchema } from "./schemas/task.js";
13
+ export type { TaskSpecification } from "./schemas/task.js";
14
+ export { SubmissionRecordSchema } from "./schemas/submission.js";
15
+ export type { SubmissionRecord } from "./schemas/submission.js";
16
+ export { DeliveryRecordSchema } from "./schemas/delivery.js";
17
+ export type { DeliveryRecord } from "./schemas/delivery.js";
18
+ export { DispatchContextSchema } from "./schemas/dispatch-context.js";
19
+ export type { DispatchContext } from "./schemas/dispatch-context.js";
20
+ export { ATTEMPT_OBSERVATION_TYPES, OBSERVATION_TYPES, ProtocolObservationSchema, SUBMISSION_OBSERVATION_TYPES, } from "./schemas/observation.js";
21
+ export type { ProtocolObservation } from "./schemas/observation.js";
22
+ export { validateDelivery, validateDispatchContext, validateObservation, validateSubmission, validateTask, } from "./validators.js";
23
+ export type { ValidationIssue, ValidationResult } from "./validators.js";
24
+ export { ERROR_RETRYABLE, TASK_EXECUTION_ERROR_CATEGORIES, isRetryable } from "./errors.js";
25
+ export type { TaskExecutionErrorCategory } from "./errors.js";
26
+ export { ATTEMPT_EVENT_TYPES, MAX_SEQUENCE, SUBMISSION_EVENT_TYPES, TASK_EXECUTION_EVENT_TYPES, formatSequence, } from "./events.js";
27
+ export { ATTEMPT_STATES, TERMINAL_STATES } from "./states.js";
28
+ export type { AttemptState, FailureBlame } from "./states.js";
29
+ export { foldObservations } from "./fold.js";
30
+ export type { AttemptDescriptor, DerivedAttemptState, FoldObservationsOptions } from "./fold.js";
31
+ export { InvalidDocumentError, sealDelivery, sealSubmission, sealTask } from "./sealing.js";
32
+ export { mergeRequirements } from "./requirements.js";
33
+ export type { ComparisonClass, EffectiveRequirements, MergeRequirementsResult } from "./requirements.js";
34
+ export { loadAdversarialManifest, loadEquivalenceExpectedDigest, loadEquivalenceTaskBytes, loadGoldenConformanceReport, loadGoldenDeliveryBytes, loadGoldenObservations, loadGoldenSubmissionBytes, loadGoldenTaskBytes, readAdversarialFile, readAdversarialJson, } from "./fixtures.js";
35
+ export type { AdversarialManifest, AdversarialManifestEntry, GoldenConformanceReport, GoldenScenario, } from "./fixtures.js";
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ // @jinn-network/task-execution-protocol — public surface (frozen at the §22 granularity).
2
+ // Pure, I/O-free reference implementation of the Jinn Task Execution Protocol v1.
3
+ export const TASK_EXECUTION_PROTOCOL_URI = "https://spec.jinn.network/profiles/task-execution/v1";
4
+ // --- sealing primitives (order, hashing, canonicalization, I-JSON) ---
5
+ export { compareCodeUnitStrings } from "./order.js";
6
+ export { documentDigest, sha256Hex } from "./hashing.js";
7
+ export { serializeCanonicalJson } from "./canonical.js";
8
+ export { IJsonNumberError, UndefinedArrayElementError, assertIJsonInteger } from "./json.js";
9
+ // --- descriptors + structural evidence seam ---
10
+ export { resourceDescriptorHasLocator } from "./descriptors.js";
11
+ // --- identity: deterministic Attempt-URI derivation (§9.2, program §7.2) ---
12
+ export { DELIVERY_MEDIA_TYPE, SUBMISSION_MEDIA_TYPE, TASK_MEDIA_TYPE, TEP_ATTEMPT_NAMESPACE, deriveAttemptUri, isValidUrnUuid, } from "./identifiers.js";
13
+ // --- family schemas (zod) ---
14
+ export { DigestMapSchema, EFFORT_TIERS, EVIDENCE_RECORD_FAMILIES, EffortTierSchema, EvidenceRecordReferenceSchema, HarnessPinSchema, IsolationPolicySchema, LoadoutSchema, ModelPinSchema, PreferencesMap, RequirementsMap, ResourceDescriptorSchema, Rfc3339, Sha256Digest, UrnUuid, } from "./schemas/common.js";
15
+ export { TaskOutputSlotSchema, TaskSpecificationSchema } from "./schemas/task.js";
16
+ export { SubmissionRecordSchema } from "./schemas/submission.js";
17
+ export { DeliveryRecordSchema } from "./schemas/delivery.js";
18
+ export { DispatchContextSchema } from "./schemas/dispatch-context.js";
19
+ export { ATTEMPT_OBSERVATION_TYPES, OBSERVATION_TYPES, ProtocolObservationSchema, SUBMISSION_OBSERVATION_TYPES, } from "./schemas/observation.js";
20
+ // --- validators ---
21
+ export { validateDelivery, validateDispatchContext, validateObservation, validateSubmission, validateTask, } from "./validators.js";
22
+ // --- error, event, and Attempt-state vocabularies (§13/§10.2/§10.3) ---
23
+ export { ERROR_RETRYABLE, TASK_EXECUTION_ERROR_CATEGORIES, isRetryable } from "./errors.js";
24
+ export { ATTEMPT_EVENT_TYPES, MAX_SEQUENCE, SUBMISSION_EVENT_TYPES, TASK_EXECUTION_EVENT_TYPES, formatSequence, } from "./events.js";
25
+ export { ATTEMPT_STATES, TERMINAL_STATES } from "./states.js";
26
+ // --- the observation fold (§10.4) + the Attempt projection (§9/§22) ---
27
+ export { foldObservations } from "./fold.js";
28
+ // --- sealing entry points (§6.1) ---
29
+ export { InvalidDocumentError, sealDelivery, sealSubmission, sealTask } from "./sealing.js";
30
+ // --- requirements/run-pinning merge (carried amendment 1, program §7.3) ---
31
+ export { mergeRequirements } from "./requirements.js";
32
+ // --- fixture loaders (golden + adversarial, §24) ---
33
+ export { loadAdversarialManifest, loadEquivalenceExpectedDigest, loadEquivalenceTaskBytes, loadGoldenConformanceReport, loadGoldenDeliveryBytes, loadGoldenObservations, loadGoldenSubmissionBytes, loadGoldenTaskBytes, readAdversarialFile, readAdversarialJson, } from "./fixtures.js";
package/dist/json.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export declare class IJsonNumberError extends Error {
5
+ readonly value: number;
6
+ constructor(value: number);
7
+ }
8
+ declare class InvalidIJsonValueError extends Error {
9
+ readonly category: "invalid-document";
10
+ constructor(message: string);
11
+ }
12
+ /**
13
+ * Retained public error for callers that distinguish malformed arrays. Explicit `undefined`
14
+ * elements and holes both violate the stack-wide dense-array rule (§7.1/§7.24).
15
+ */
16
+ export declare class UndefinedArrayElementError extends InvalidIJsonValueError {
17
+ constructor();
18
+ }
19
+ /** Sealed families admit only exact I-JSON integers; fractional quantities are strings (§6.1). */
20
+ export declare function assertIJsonInteger(value: number): void;
21
+ /** Validate and snapshot the fail-closed JSON value domain before canonical emission. */
22
+ export declare function cloneIJsonValue(value: unknown): JsonValue;
23
+ export {};