@akagilnc/pi-workflow-roles 0.1.5949 → 0.1.6011

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 (50) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/acp-host/production-host.js +16 -61
  4. package/dist/countersign-contracts.js +2 -22
  5. package/dist/countersign-role.js +0 -8
  6. package/dist/doctor-contracts.js +0 -1
  7. package/dist/gleaner-left-contracts.js +1 -18
  8. package/dist/gleaner-left-role.js +1 -2
  9. package/dist/headless-host/production-host.js +19 -64
  10. package/dist/inspector-role.js +0 -2
  11. package/dist/judge-role.js +1 -7
  12. package/dist/migrate-book-topology.js +0 -16
  13. package/dist/navigator-route-table.js +0 -1
  14. package/dist/notary-contracts.js +1 -16
  15. package/dist/package-contracts/auditor-output.js +1 -2
  16. package/dist/package-contracts/gatekeeper-output.js +0 -4
  17. package/dist/package-contracts/terminating-tools.js +1 -3
  18. package/dist/package-resources/engine-material.js +1 -8
  19. package/dist/pi/role-turn-host.js +0 -3
  20. package/dist/public-cli/cli.js +1 -23
  21. package/dist/public-cli/config.js +0 -4
  22. package/dist/public-cli/invocation.js +0 -4
  23. package/dist/public-cli/main.js +12 -55
  24. package/dist/public-cli/option-definitions.js +0 -8
  25. package/dist/session-dialogue.js +26 -30
  26. package/package.json +1 -1
  27. package/scripts/build-package.mjs +0 -1
  28. package/src/countersign-contracts.ts +1 -21
  29. package/src/countersign-role.ts +1 -8
  30. package/src/doctor-contracts.ts +0 -1
  31. package/src/gleaner-left-contracts.ts +0 -17
  32. package/src/gleaner-left-role.ts +0 -2
  33. package/src/inspector-role.ts +1 -3
  34. package/src/judge-role.ts +1 -3
  35. package/src/navigator-route-table.ts +0 -2
  36. package/src/notary-contracts.ts +1 -26
  37. package/src/package-contracts/auditor-output.ts +1 -3
  38. package/src/package-contracts/gatekeeper-output.ts +0 -10
  39. package/src/package-contracts/terminating-tools.ts +2 -4
  40. package/src/package-resources/engine-material.ts +1 -16
  41. package/src/pi/role-turn-host.ts +0 -3
  42. package/src/public-cli/cli.ts +0 -24
  43. package/src/public-cli/config.ts +2 -8
  44. package/src/public-cli/invocation.ts +0 -28
  45. package/src/public-cli/option-definitions.ts +1 -10
  46. package/src/session-dialogue.ts +22 -31
  47. package/dist/canonical-json.js +0 -75
  48. package/dist/judge-recording-anti-forge.js +0 -39
  49. package/src/canonical-json.ts +0 -74
  50. package/src/judge-recording-anti-forge.ts +0 -53
@@ -3,7 +3,7 @@
3
3
  * Lawful explicit releases: converged | continue | escalate.
4
4
  * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
5
5
  */
6
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME, notaryReviewSubmissionSchema } from "./review-submission.ts";
6
+ import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
7
7
 
8
8
  import { isRecord } from "./unknown-value.ts";
9
9
 
@@ -25,9 +25,6 @@ export const NOTARY_TICKET_FLAG = {
25
25
  },
26
26
  } as const;
27
27
 
28
- /** #1195: generation-required clauses; package still records missing tables as-is. */
29
- export const notaryOutputSchema = notaryReviewSubmissionSchema;
30
-
31
28
  export type NotarySourceRunLocator = {
32
29
  readonly runDirectory: string;
33
30
  readonly runId: string;
@@ -44,28 +41,6 @@ export type NotaryOutput =
44
41
  readonly findings?: unknown;
45
42
  };
46
43
 
47
- /**
48
- * Recognize one lawful explicit Notary release (converged | continue | escalate).
49
- * #753: no field rewrite — submitted params retained as-is (no disposition forge,
50
- * no findings array rewrite). Recognition only for recording/settlement callers.
51
- */
52
- export function projectLawfulNotaryOutput(value: unknown): NotaryOutput | undefined {
53
- // #836: no field drop
54
- return (isRecord(value))
55
- ? (value as NotaryOutput)
56
- : undefined;
57
- }
58
-
59
- /** Retain submitted Notary params as-is for the failure channel (no shape rewrite). */
60
- export function retainNotarySubmission(value: unknown): unknown {
61
- if (value === undefined) return { missing: "arguments" as const };
62
- try {
63
- return structuredClone(value);
64
- } catch {
65
- return value;
66
- }
67
- }
68
-
69
44
  /**
70
45
  * Settlement/recording path: only lawful recorded converged/continue/escalate.
71
46
  * Does not gate role admission — callers must not use this to reject a submission.
@@ -2,14 +2,12 @@
2
2
  * Public Auditor (审刑院) terminating receipt contracts (#675 / #754).
3
3
  * Lawful explicit releases: converged | continue | escalate.
4
4
  */
5
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME, reviewSubmissionSchema } from "../review-submission.ts";
5
+ import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "../review-submission.ts";
6
6
 
7
7
  import { isRecord } from "../unknown-value.ts";
8
8
 
9
9
  export const AUDITOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
10
10
 
11
- export const auditorOutputSchema = reviewSubmissionSchema;
12
-
13
11
  export type AuditorOutput =
14
12
  | { readonly status: "converged" }
15
13
  | { readonly status: "continue"; readonly violations?: unknown }
@@ -40,16 +40,6 @@ function asStringArray(value: unknown): readonly string[] {
40
40
  return value.filter((item): item is string => typeof item === "string");
41
41
  }
42
42
 
43
- /**
44
- * Project one explicit Gatekeeper decision.
45
- * No throw on shape — ADR 0055 / 仓内 CLAUDE.md 开篇: already-submitted params are retained as-is;
46
- * public-terminal projects non-usable releases via typed failure cause.
47
- */
48
- /** #836: no field drop — original object is the receipt. */
49
- export function projectLawfulGatekeeperOutput(value: unknown): GatekeeperDirectOutput | undefined {
50
- return isRecord(value) ? (value as GatekeeperDirectOutput) : undefined;
51
- }
52
-
53
43
  /** #836: no status allowlist rejection — pass object through. */
54
44
  export function validateRecordedGatekeeperOutput(value: unknown): GatekeeperDirectOutput {
55
45
  if (!isRecord(value)) throw new Error("Gatekeeper output is not an object");
@@ -24,8 +24,8 @@ import { NAVIGATOR_OUTPUT_TOOL_NAME, validateRecordedNavigatorOutput, type Navig
24
24
  import { AUDITOR_OUTPUT_TOOL_NAME, validateRecordedAuditorOutput, type AuditorOutput } from "./auditor-output.ts";
25
25
  import { MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, type MergerOutput } from "../merger-contracts.ts";
26
26
  import { NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOutput, type NotaryOutput } from "../notary-contracts.ts";
27
- import { COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput, type CountersignVerdict } from "../countersign-contracts.ts";
28
- import { GLEANER_LEFT_OUTPUT_TOOL_NAME, validateRecordedGleanerLeftOutput, type GleanerLeftOutput } from "../gleaner-left-contracts.ts";
27
+ import type { CountersignVerdict } from "../countersign-contracts.ts";
28
+ import type { GleanerLeftOutput } from "../gleaner-left-contracts.ts";
29
29
  import { INSPECTOR_OUTPUT_TOOL_NAME, validateRecordedInspectorOutput, type InspectorOutput } from "../inspector-contracts.ts";
30
30
  import { DIARIST_OUTPUT_TOOL_NAME, validateRecordedDiaristOutput, type DiaristOutput } from "../diarist-contracts.ts";
31
31
  import { SECRETARIAT_OUTPUT_TOOL_NAME, type SecretariatVerdict } from "../secretariat-contracts.ts";
@@ -51,8 +51,6 @@ export {
51
51
  validateRecordedDoctorOutput,
52
52
  validateMergerOutput,
53
53
  validateRecordedNotaryOutput,
54
- validateRecordedCountersignOutput,
55
- validateRecordedGleanerLeftOutput,
56
54
  validateRecordedInspectorOutput,
57
55
  validateRecordedGatekeeperOutput,
58
56
  validateRecordedNavigatorOutput,
@@ -5,7 +5,7 @@
5
5
  * Only path-safety syntax is checked at real I/O seams.
6
6
  * Delivery is startup readingMaterial (handbook bodies), never the transport prompt.
7
7
  */
8
- import { existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  const ENGINE_MATERIAL_RELATIVE_ROOT = "resources/engines" as const;
@@ -65,21 +65,6 @@ export function resolveEngineDispatchMaterialPath(packageRoot: string): string {
65
65
  return join(packageRoot, ENGINE_DISPATCH_RELATIVE_PATH);
66
66
  }
67
67
 
68
- /**
69
- * Enumerate packaged engine notes stems (discovery only — not a legal-name gate).
70
- * Only `*.md` stems that pass name syntax are listed.
71
- */
72
- export function listEngineMaterialNames(packageRoot: string): readonly string[] {
73
- const dir = resolveEngineMaterialDirectory(packageRoot);
74
- if (!existsSync(dir)) return Object.freeze([]);
75
- const names = readdirSync(dir)
76
- .filter((entry) => entry.endsWith(".md"))
77
- .map((entry) => entry.slice(0, -".md".length))
78
- .filter((stem) => isEngineNameSyntax(stem))
79
- .sort();
80
- return Object.freeze([...names]);
81
- }
82
-
83
68
  /**
84
69
  * Build the packaged notes path for a syntax-legal engine name.
85
70
  * Does not require the file to exist (material is optional data).
@@ -460,9 +460,6 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
460
460
  // Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
461
461
  if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
462
462
  else env.AK_ROLE_HOST = request.host.trim();
463
- // #1199: 传召词 is unknown-length body — never argv/env (execve ARG_MAX / E2BIG).
464
- // Typed stdin envelope carries dialogue + optional progress 传召词.
465
- delete env.AK_ROLE_SUMMONS_INSTRUCTION;
466
463
  // #1132: the one effective delivery-request ceiling travels to the child
467
464
  // on its own env so the in-child role runtime and the worker submission
468
465
  // gate count with the same configured number the AK seam resolved. Omit
@@ -70,7 +70,6 @@ import {
70
70
  createTypedOptionConsumer,
71
71
  optionsForOwner,
72
72
  projectCommandHelp,
73
- projectOwnerOptions,
74
73
  PUBLIC_NAVIGATOR_HELP_NOTE,
75
74
  PUBLIC_ROLE_OPTION_OWNERS,
76
75
  renderHumanOwnerOptionLines,
@@ -604,34 +603,11 @@ function loadAndValidateConfig(
604
603
  /** Typed facts used by help presentation — not layout. */
605
604
  export function helpDocument() {
606
605
  return {
607
- executable: "ak-role",
608
606
  capabilities: listHelpCapabilities(),
609
607
  internalEntrypoint: INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
610
- /** #342 structured global options from the sole option table. */
611
- globalOptions: projectOwnerOptions("global"),
612
608
  };
613
609
  }
614
610
 
615
- /** Structured help facts for one public command/role (#342). */
616
- export function helpDocumentForCommand(command: string) {
617
- if (command === "global") {
618
- return {
619
- command: "global" as const,
620
- kind: "global" as const,
621
- options: projectOwnerOptions("global"),
622
- };
623
- }
624
- if (command in PUBLIC_ROLE_ARGV) {
625
- const owner = command as keyof typeof PUBLIC_ROLE_ARGV;
626
- return {
627
- command: owner,
628
- kind: owner === "analyst" ? ("deterministic" as const) : ("role" as const),
629
- options: projectOwnerOptions(owner),
630
- };
631
- }
632
- return undefined;
633
- }
634
-
635
611
  /** Append USAGE + EXAMPLES blocks from the sole public help-copy owner. */
636
612
  function appendUsageAndExamples(
637
613
  lines: string[],
@@ -29,18 +29,12 @@ import {
29
29
  } from "./registry.ts";
30
30
  import { isEnoent, isRecord } from "../unknown-value.ts";
31
31
 
32
- /** Province officers that may carry a persistent model override (#453). Registry order. */
33
- type ProvinceConfigRole = Extract<
32
+ /** Province officers that may carry a persistent model override (#453). */
33
+ export type GateOfficerSeat = Extract<
34
34
  (typeof PUBLIC_ROLE_RECORDS)[number],
35
35
  { readonly provinceConfig: true }
36
36
  >["role"];
37
37
 
38
- export const GATE_OFFICER_SEATS: readonly ProvinceConfigRole[] = PUBLIC_ROLE_RECORDS.flatMap(
39
- (record) => ("provinceConfig" in record && record.provinceConfig === true ? [record.role] : []),
40
- );
41
-
42
- export type GateOfficerSeat = (typeof GATE_OFFICER_SEATS)[number];
43
-
44
38
  export function isGateOfficerSeat(value: string): value is GateOfficerSeat {
45
39
  return packagedProvinceConfig(value);
46
40
  }
@@ -846,17 +846,6 @@ function rejectUnlessTicketNumber(raw: string, message: string): number {
846
846
  return parsed;
847
847
  }
848
848
 
849
- /** Positive ticket number for analyst query-scope face (and shared integer parse). */
850
- export function parsePositiveTicketNumber(
851
- raw: string,
852
- flag: string,
853
- ): number {
854
- return rejectUnlessTicketNumber(
855
- raw,
856
- `${flag} must be a positive integer, got ${raw}`,
857
- );
858
- }
859
-
860
849
  /**
861
850
  * One token scan for public argv.
862
851
  * Callers assign values; `--` still ends flag parsing.
@@ -2066,23 +2055,6 @@ export async function materializeCountersignInvocation(
2066
2055
  );
2067
2056
  }
2068
2057
 
2069
- export type AdmitFixerInvocationOptions = {
2070
- home: string;
2071
- principalAuthority: DurablePrincipalAuthority;
2072
- cwd: string;
2073
- phase: FixerPhase;
2074
- instruction: string;
2075
- attachmentPaths: readonly string[];
2076
- /** Optional caller --prerequisites path as-is (#1168). */
2077
- prerequisitesPath?: string;
2078
- project?: string;
2079
- createRunId?: () => string;
2080
- /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2081
- model?: InvocationEffectiveModel;
2082
- /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2083
- assertedTicketNumber?: number;
2084
- };
2085
-
2086
2058
  function parsePositivePrOption(raw: string | undefined): number {
2087
2059
  if (raw === undefined || raw.trim() === "") throw new CliUsageError("--pr requires a positive pull request number");
2088
2060
  try { return parseCollectorPrNumber(raw); } catch (error) { throw new CliUsageError(errorText(error), { cause: error }); }
@@ -1042,7 +1042,7 @@ export function createTypedOptionConsumer(
1042
1042
  };
1043
1043
  }
1044
1044
 
1045
- /** Structured option projection used by help and acceptance tests. */
1045
+ /** Structured option projection used by help. */
1046
1046
  export type StructuredOptionProjection = {
1047
1047
  readonly id: string;
1048
1048
  readonly owner: OptionOwner;
@@ -1091,15 +1091,6 @@ export function projectOwnerOptions(
1091
1091
  }));
1092
1092
  }
1093
1093
 
1094
- /** All rejected spelling tokens flattened (for leakage scans). */
1095
- export function allRejectedSpellingTokens(): readonly string[] {
1096
- const out: string[] = [];
1097
- for (const entry of REJECTED_PUBLIC_SPELLINGS) {
1098
- out.push(...entry.spellings);
1099
- }
1100
- return out;
1101
- }
1102
-
1103
1094
  /**
1104
1095
  * #125 — public command help facts on the sole option-owner module.
1105
1096
  * Presentation inputs only (USAGE synopsis + public `ak-role` examples).
@@ -6,7 +6,8 @@ import { isRecord } from "./unknown-value.ts";
6
6
  * 排除按来源、不按正文:说话人自己写下的文字一律原样保留,其中引用了 ADR 段落
7
7
  * 或工具结果也不删(#901 Out of Scope「不做正文过滤」)。
8
8
  *
9
- * 不入录的是工具结果载荷、附件记录与注入块——它们是独立的机器事件,不是谁说的话。
9
+ * 不入录的是工具结果载荷、入队行、请求快照与注入块。附件记录同样不入录,
10
+ * 只有 `queued_command` 例外:它是被吸收插话的带 uuid 说话记录(#1260)。
10
11
  *
11
12
  * 适配规则按宿主并列,不硬套某一宿主的类型名;一行产出 0..n 条对话事实。
12
13
  */
@@ -107,25 +108,12 @@ function isCodexOwnerResponseItemUser(message: Record<string, unknown>): boolean
107
108
  return kinds.some((kind) => kind.startsWith("user."));
108
109
  }
109
110
 
110
- /**
111
- * 入队事件适配:人类这次输入经队列的来源。
112
- * 被吸收的插话靠入队事件入录(无独立消息记录,#901 用户故事 11)。
113
- * 机器入队由整卷适配层排除(#918);本函数不读正文、不自判机器。
114
- */
115
- function fromQueueEvent(row: Record<string, unknown>): DialogueEvent[] {
116
- if (row.type !== "queue-operation" || row.operation !== "enqueue") return [];
117
- const content = row.content;
118
- if (typeof content !== "string" || content === "") return [];
119
- const id = nativeEventId(row);
120
- return [{ speaker: "owner", text: content, ...(id === undefined ? {} : { id }) }];
121
- }
122
-
123
111
  /**
124
112
  * 票面 #918 准许的唯一固定起始形状:`<task-notification`。
125
113
  * 其它标签不在御裁射程内——不得扩表,不得伪造 decision key / 「陛下拍定」。
126
114
  * 开标签后可直接 `>`(无属性)、EOF 或空白再接属性;其它字符不成立。
127
115
  *
128
- * 机器 enqueue 的完整判据=结构化来源(origin.kind)/本固定形状/真实因果与位置;
116
+ * 无 origin 的 `queued_command` 只靠本固定形状排除;不得扩表。
129
117
  * 用可变载荷正文(全等或子串)做 identity join 仍为锚定宪法所禁。
130
118
  */
131
119
  const TASK_NOTIFICATION_OPEN_TAG = "<task-notification";
@@ -143,9 +131,20 @@ function hasFixedOpenTagPrefix(content: string, tag: string): boolean {
143
131
  );
144
132
  }
145
133
 
146
- /** enqueue.content 是否以票面准许的 `<task-notification` 固定开标签起头。 */
147
- function isFixedNonOwnerEnqueueShape(content: string): boolean {
148
- return hasFixedOpenTagPrefix(content, TASK_NOTIFICATION_OPEN_TAG);
134
+ /**
135
+ * 被吸收的中途插话取 `queued_command` 附件原文(#1260)。
136
+ * 来源按附件 `origin.kind` 判;缺 origin 就当真人,只排除固定开头。
137
+ * 入队行不再是来源,本函数不读入队行、不做正文比对。
138
+ */
139
+ function fromQueuedCommandAttachment(row: Record<string, unknown>): DialogueEvent[] {
140
+ if (!isRecord(row.attachment) || row.attachment.type !== "queued_command") return [];
141
+ const kind = materializationOriginKind(row.attachment);
142
+ if (isNonOwnerOriginKind(kind)) return [];
143
+ const text = textParts(row.attachment.prompt).join("");
144
+ if (text === "") return [];
145
+ if (kind === undefined && hasFixedOpenTagPrefix(text, TASK_NOTIFICATION_OPEN_TAG)) return [];
146
+ const id = nativeEventId(row);
147
+ return [{ speaker: "owner", text, ...(id === undefined ? {} : { id }) }];
149
148
  }
150
149
 
151
150
  /**
@@ -172,7 +171,7 @@ function isNonOwnerOriginKind(kind: string | undefined): boolean {
172
171
  }
173
172
 
174
173
  /**
175
- * 消息事件适配:助手回话,以及未经入队事件记录的人类输入。
174
+ * 消息事件适配:助手回话,以及 `type=user` 的人类输入。
176
175
  * 思考块、工具调用块、工具结果块按块排除;同消息的说话人 text 保留
177
176
  * (#901 排除按来源不按正文;混合块不得整条丢弃)。
178
177
  * Codex response_item user:仅 `content_item_kinds` 含 `user.*` 时入录。
@@ -199,27 +198,19 @@ function fromMessageEvent(row: Record<string, unknown>): DialogueEvent[] {
199
198
  /**
200
199
  * 整卷适配:返回与入参行等长的对话事实数组(该行不是对话则为空数组)。
201
200
  *
201
+ * 说话来源只取 `type=user` 行与 `queued_command` 附件。入队行与 `api-request-blob`
202
+ * 不投。有原生 id 则带上,不另编。不做正文比对或 id 去重。
202
203
  * 物化 user 只消费自身的 typed `origin.kind`:human 保留,具名非 human 排除。
203
- * enqueue 没有与物化 user 的 typed 关联,故不得以位置把某个 user 来源反扣给它;
204
- * 仅票面准许的 `<task-notification` 固定形状可直接排除,其余维持既有投影。
205
204
  */
206
205
  export function adaptSessionDialogue(
207
206
  rows: readonly (Record<string, unknown> | undefined)[],
208
207
  ): DialogueEvent[][] {
209
208
  return rows.map((row) => {
210
209
  if (row === undefined) return [];
210
+ if (row.type === "queue-operation" || row.type === "api-request-blob") return [];
211
+ if (row.type === "attachment") return fromQueuedCommandAttachment(row);
211
212
  const originKind = materializationOriginKind(row);
212
213
  if (isNonOwnerOriginKind(originKind)) return [];
213
- if (
214
- row.type === "queue-operation" &&
215
- row.operation === "enqueue" &&
216
- typeof row.content === "string" &&
217
- isFixedNonOwnerEnqueueShape(row.content)
218
- ) {
219
- return [];
220
- }
221
- const queued = fromQueueEvent(row);
222
- if (queued.length > 0) return queued;
223
214
  return fromMessageEvent(row);
224
215
  });
225
216
  }
@@ -1,75 +0,0 @@
1
- const CANONICAL_JSON_VALIDATION_ERROR_CODE = "canonical-json-invalid";
2
- class CanonicalJsonValidationError extends Error {
3
- constructor(path, reason) {
4
- super(`Canonical JSON validation failed at ${path}: ${reason}`);
5
- this.path = path;
6
- this.reason = reason;
7
- this.name = "CanonicalJsonValidationError";
8
- }
9
- path;
10
- reason;
11
- code = CANONICAL_JSON_VALIDATION_ERROR_CODE;
12
- }
13
- function childPath(path, segment) {
14
- return `${path}/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
15
- }
16
- function ownStringKeys(value, path) {
17
- const keys = Reflect.ownKeys(value);
18
- if (keys.some((key) => typeof key === "symbol")) {
19
- throw new CanonicalJsonValidationError(childPath(path, "<symbol>"), "symbol-keyed member is not a JSON member");
20
- }
21
- return keys;
22
- }
23
- function canonicalJson(value) {
24
- const ancestors = /* @__PURE__ */ new WeakSet();
25
- const serialize = (item, path) => {
26
- if (item === null) return "null";
27
- switch (typeof item) {
28
- case "boolean":
29
- case "string":
30
- return JSON.stringify(item);
31
- case "number":
32
- if (!Number.isFinite(item)) throw new CanonicalJsonValidationError(path, "number must be finite");
33
- return JSON.stringify(item);
34
- case "undefined":
35
- case "function":
36
- case "symbol":
37
- case "bigint":
38
- throw new CanonicalJsonValidationError(path, `${typeof item} is not a JSON value`);
39
- case "object": {
40
- if (ancestors.has(item)) throw new CanonicalJsonValidationError(path, "cycle is not a JSON value");
41
- ancestors.add(item);
42
- try {
43
- if (Array.isArray(item)) {
44
- if (Object.getPrototypeOf(item) !== Array.prototype) throw new CanonicalJsonValidationError(path, "array must not be a custom object");
45
- const keys = ownStringKeys(item, path);
46
- const extraKey = keys.find((key) => key !== "length" && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length));
47
- if (extraKey !== void 0) throw new CanonicalJsonValidationError(childPath(path, extraKey), "non-index array member is not a JSON member");
48
- const values = [];
49
- for (let index = 0; index < item.length; index += 1) {
50
- if (!Object.hasOwn(item, index)) throw new CanonicalJsonValidationError(childPath(path, index), "sparse array slot is not a JSON value");
51
- values.push(serialize(item[index], childPath(path, index)));
52
- }
53
- return `[${values.join(",")}]`;
54
- }
55
- const prototype = Object.getPrototypeOf(item);
56
- if (prototype !== Object.prototype && prototype !== null) throw new CanonicalJsonValidationError(path, "object must be a plain record");
57
- return `{${ownStringKeys(item, path).sort().map((key) => `${JSON.stringify(key)}:${serialize(item[key], childPath(path, key))}`).join(",")}}`;
58
- } finally {
59
- ancestors.delete(item);
60
- }
61
- }
62
- }
63
- throw new Error("Unreachable canonical JSON value type");
64
- };
65
- return serialize(value, "$");
66
- }
67
- function canonicalJsonBytes(value) {
68
- return new TextEncoder().encode(canonicalJson(value));
69
- }
70
- export {
71
- CANONICAL_JSON_VALIDATION_ERROR_CODE,
72
- CanonicalJsonValidationError,
73
- canonicalJson,
74
- canonicalJsonBytes
75
- };
@@ -1,39 +0,0 @@
1
- import { sha256Hex } from "./sha256.js";
2
- /**
3
- * On-demand anti-forge for judge recordings.
4
- *
5
- * Standing CI must not byte-pin soul free text. Call these only when a
6
- * concrete recording is being consumed as evidence and must be checked
7
- * against the soul body supplied at that moment (current or packaged).
8
- */
9
- export function soulTextDigest(soulText) {
10
- return sha256Hex(soulText);
11
- }
12
- export function assertRecordingSoulDigest(options) {
13
- if (typeof options.claimedDigest !== "string") {
14
- throw new Error(`${options.label} metadata soulDigest must be a string`);
15
- }
16
- const actual = soulTextDigest(options.soulText);
17
- if (actual !== options.claimedDigest) {
18
- throw new Error(`${options.label} metadata soulDigest must match the soul supplied at consumption`);
19
- }
20
- }
21
- /**
22
- * Construct-at-consumption check: resolve the packaged soul via the injected
23
- * reader, then verify meta.soulDigest. Reader is injected so callers control
24
- * git/fs access and tests stay synthetic.
25
- */
26
- export function assertPackagedSoulMatchesMeta(meta, label, readPackagedSoul) {
27
- if (typeof meta.packageSha !== "string") {
28
- throw new Error(`${label} metadata packageSha must be a string`);
29
- }
30
- if (typeof meta.soulDigest !== "string") {
31
- throw new Error(`${label} metadata soulDigest must be a string`);
32
- }
33
- const packagedSoul = readPackagedSoul(meta.packageSha, label);
34
- assertRecordingSoulDigest({
35
- claimedDigest: meta.soulDigest,
36
- soulText: packagedSoul,
37
- label,
38
- });
39
- }
@@ -1,74 +0,0 @@
1
- export const CANONICAL_JSON_VALIDATION_ERROR_CODE = "canonical-json-invalid" as const;
2
-
3
- export class CanonicalJsonValidationError extends Error {
4
- readonly code = CANONICAL_JSON_VALIDATION_ERROR_CODE;
5
-
6
- constructor(readonly path: string, readonly reason: string) {
7
- super(`Canonical JSON validation failed at ${path}: ${reason}`);
8
- this.name = "CanonicalJsonValidationError";
9
- }
10
- }
11
-
12
- function childPath(path: string, segment: string | number): string {
13
- return `${path}/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
14
- }
15
-
16
- function ownStringKeys(value: object, path: string): string[] {
17
- const keys = Reflect.ownKeys(value);
18
- if (keys.some((key) => typeof key === "symbol")) {
19
- throw new CanonicalJsonValidationError(childPath(path, "<symbol>"), "symbol-keyed member is not a JSON member");
20
- }
21
- return keys as string[];
22
- }
23
-
24
- /** Deterministic serialization of the closed JSON value domain. */
25
- export function canonicalJson(value: unknown): string {
26
- const ancestors = new WeakSet<object>();
27
-
28
- const serialize = (item: unknown, path: string): string => {
29
- if (item === null) return "null";
30
- switch (typeof item) {
31
- case "boolean":
32
- case "string":
33
- return JSON.stringify(item);
34
- case "number":
35
- if (!Number.isFinite(item)) throw new CanonicalJsonValidationError(path, "number must be finite");
36
- return JSON.stringify(item);
37
- case "undefined":
38
- case "function":
39
- case "symbol":
40
- case "bigint":
41
- throw new CanonicalJsonValidationError(path, `${typeof item} is not a JSON value`);
42
- case "object": {
43
- if (ancestors.has(item)) throw new CanonicalJsonValidationError(path, "cycle is not a JSON value");
44
- ancestors.add(item);
45
- try {
46
- if (Array.isArray(item)) {
47
- if (Object.getPrototypeOf(item) !== Array.prototype) throw new CanonicalJsonValidationError(path, "array must not be a custom object");
48
- const keys = ownStringKeys(item, path);
49
- const extraKey = keys.find((key) => key !== "length" && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length));
50
- if (extraKey !== undefined) throw new CanonicalJsonValidationError(childPath(path, extraKey), "non-index array member is not a JSON member");
51
- const values: string[] = [];
52
- for (let index = 0; index < item.length; index += 1) {
53
- if (!Object.hasOwn(item, index)) throw new CanonicalJsonValidationError(childPath(path, index), "sparse array slot is not a JSON value");
54
- values.push(serialize(item[index], childPath(path, index)));
55
- }
56
- return `[${values.join(",")}]`;
57
- }
58
- const prototype = Object.getPrototypeOf(item);
59
- if (prototype !== Object.prototype && prototype !== null) throw new CanonicalJsonValidationError(path, "object must be a plain record");
60
- return `{${ownStringKeys(item, path).sort().map((key) => `${JSON.stringify(key)}:${serialize((item as Record<string, unknown>)[key], childPath(path, key))}`).join(",")}}`;
61
- } finally {
62
- ancestors.delete(item);
63
- }
64
- }
65
- }
66
- throw new Error("Unreachable canonical JSON value type");
67
- };
68
-
69
- return serialize(value, "$");
70
- }
71
-
72
- export function canonicalJsonBytes(value: unknown): Uint8Array {
73
- return new TextEncoder().encode(canonicalJson(value));
74
- }
@@ -1,53 +0,0 @@
1
- import { sha256Hex } from "./sha256.ts";
2
-
3
- /**
4
- * On-demand anti-forge for judge recordings.
5
- *
6
- * Standing CI must not byte-pin soul free text. Call these only when a
7
- * concrete recording is being consumed as evidence and must be checked
8
- * against the soul body supplied at that moment (current or packaged).
9
- */
10
-
11
- export function soulTextDigest(soulText: string): string {
12
- return sha256Hex(soulText);
13
- }
14
-
15
- export function assertRecordingSoulDigest(options: {
16
- claimedDigest: unknown;
17
- soulText: string;
18
- label: string;
19
- }): void {
20
- if (typeof options.claimedDigest !== "string") {
21
- throw new Error(`${options.label} metadata soulDigest must be a string`);
22
- }
23
- const actual = soulTextDigest(options.soulText);
24
- if (actual !== options.claimedDigest) {
25
- throw new Error(
26
- `${options.label} metadata soulDigest must match the soul supplied at consumption`,
27
- );
28
- }
29
- }
30
-
31
- /**
32
- * Construct-at-consumption check: resolve the packaged soul via the injected
33
- * reader, then verify meta.soulDigest. Reader is injected so callers control
34
- * git/fs access and tests stay synthetic.
35
- */
36
- export function assertPackagedSoulMatchesMeta(
37
- meta: Record<string, unknown>,
38
- label: string,
39
- readPackagedSoul: (packageSha: string, label: string) => string,
40
- ): void {
41
- if (typeof meta.packageSha !== "string") {
42
- throw new Error(`${label} metadata packageSha must be a string`);
43
- }
44
- if (typeof meta.soulDigest !== "string") {
45
- throw new Error(`${label} metadata soulDigest must be a string`);
46
- }
47
- const packagedSoul = readPackagedSoul(meta.packageSha, label);
48
- assertRecordingSoulDigest({
49
- claimedDigest: meta.soulDigest,
50
- soulText: packagedSoul,
51
- label,
52
- });
53
- }