@stalfh233/omc-cli 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. package/skills/omc-write-hazards.md +87 -0
@@ -0,0 +1,102 @@
1
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+ export class ApprovalTokenRejectedError extends Error {
3
+ reason;
4
+ constructor(reason) {
5
+ super(`approval token rejected: ${reason}`);
6
+ this.reason = reason;
7
+ }
8
+ }
9
+ function canonical(value) {
10
+ return JSON.stringify(sort(value));
11
+ }
12
+ function sort(value) {
13
+ if (Array.isArray(value))
14
+ return value.map(sort);
15
+ if (value && typeof value === "object")
16
+ return Object.fromEntries(Object.entries(value)
17
+ .sort(([a], [b]) => a.localeCompare(b))
18
+ .map(([k, v]) => [k, sort(v)]));
19
+ return value;
20
+ }
21
+ export function packageFingerprint(plan) {
22
+ return createHash("sha256")
23
+ .update(canonical({
24
+ environment: plan.environment,
25
+ environmentFingerprint: plan.environmentFingerprint,
26
+ actions: plan.actions.map((a) => ({ tool: a.tool, args: sort(a.args) })),
27
+ }))
28
+ .digest("hex");
29
+ }
30
+ export class ApprovalTokenService {
31
+ key;
32
+ ttlMs;
33
+ now;
34
+ used = new Set();
35
+ constructor(options) {
36
+ const key = Buffer.from(options.key, "base64");
37
+ if (key.length !== 32)
38
+ throw new Error("approval token key must be exactly 32 bytes (base64).");
39
+ this.key = key;
40
+ this.ttlMs = options.ttlMs ?? 10 * 60 * 1000;
41
+ this.now = options.now ?? Date.now;
42
+ }
43
+ requestApproval(plan) {
44
+ const approvalId = randomUUID();
45
+ const fingerprint = packageFingerprint(plan);
46
+ const issuedAt = this.now();
47
+ const expiresAt = issuedAt + this.ttlMs;
48
+ const payload = canonical({
49
+ v: 1,
50
+ approvalId,
51
+ packageFingerprint: fingerprint,
52
+ issuedAt,
53
+ expiresAt,
54
+ });
55
+ const sig = createHmac("sha256", this.key).update(payload).digest("base64url");
56
+ const token = Buffer.from(payload, "utf8").toString("base64url") + "." + sig;
57
+ return {
58
+ token,
59
+ approvalId,
60
+ packageFingerprint: fingerprint,
61
+ expiresAt: new Date(expiresAt).toISOString(),
62
+ ttlSeconds: Math.round(this.ttlMs / 1000),
63
+ singleUse: true,
64
+ };
65
+ }
66
+ verify(plan, token) {
67
+ const parts = token.split(".");
68
+ if (parts.length !== 2 || !parts[0] || !parts[1])
69
+ throw new ApprovalTokenRejectedError("token-malformed");
70
+ let payload;
71
+ try {
72
+ payload = Buffer.from(parts[0], "base64url").toString("utf8");
73
+ }
74
+ catch {
75
+ throw new ApprovalTokenRejectedError("token-malformed");
76
+ }
77
+ let decoded;
78
+ try {
79
+ decoded = JSON.parse(payload);
80
+ }
81
+ catch {
82
+ throw new ApprovalTokenRejectedError("token-malformed");
83
+ }
84
+ const expectedSig = createHmac("sha256", this.key).update(payload).digest("base64url");
85
+ if (!timingSafeEqualB64(parts[1], expectedSig))
86
+ throw new ApprovalTokenRejectedError("token-signature-invalid");
87
+ if (this.used.has(String(decoded.approvalId)))
88
+ throw new ApprovalTokenRejectedError("token-already-used");
89
+ if (Number(decoded.expiresAt) <= this.now())
90
+ throw new ApprovalTokenRejectedError("token-expired");
91
+ if (decoded.packageFingerprint !== packageFingerprint(plan))
92
+ throw new ApprovalTokenRejectedError("package-fingerprint-mismatch");
93
+ this.used.add(String(decoded.approvalId));
94
+ return { approvalId: String(decoded.approvalId) };
95
+ }
96
+ }
97
+ function timingSafeEqualB64(a, b) {
98
+ const ba = Buffer.from(a);
99
+ const bb = Buffer.from(b);
100
+ return ba.length === bb.length && timingSafeEqual(ba, bb);
101
+ }
102
+ //# sourceMappingURL=approval-token.js.map
package/dist/args.js ADDED
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The tool-input vocabulary shared by every governed tool: the governance
4
+ * arguments and the write-protocol one-liner. Kept apart from the read/write/
5
+ * package pipelines because it is the axis they all depend on (16 lanes import
6
+ * `environmentArg`), not a pipeline concern itself.
7
+ */
8
+ export const environmentArg = z.string().optional().describe("named environment (defaults to OMC_ENV / .omc-env / default)");
9
+ export const capabilityArg = z
10
+ .string()
11
+ .optional()
12
+ .describe('覆盖清单 lane claim:取 lane id(如 "W1-list-view"、"model-management"),不是能力昵称("listview" 会被拒并提示最接近的 lane id);一般省略——省略即自动按工具所属 lane 校验');
13
+ export const confirmProductionArg = z
14
+ .boolean()
15
+ .optional()
16
+ .describe("production 环境的写操作必须显式携带 true(高危确认,记入审计工件);非生产环境忽略");
17
+ /**
18
+ * Appended to every write-tool description (low-level via writeTool, high-level
19
+ * at their registration). Whether a write runs is decided by the environment's
20
+ * `writable` permission; a production environment additionally requires the
21
+ * per-write `confirmProduction`. The human gate stays the conversational draft
22
+ * review (omc-config-draft-review).
23
+ */
24
+ export const WRITE_PROTOCOL = "写入由环境 writable 决定(只读环境拒写);生产环境的写必须带 confirmProduction:true(高危确认,记入审计);写前仍走对话层草稿确认(omc-config-draft-review)。";
25
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1,119 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+ export const CLI_DEFAULT_OUTPUT_BYTES = 64 * 1024;
5
+ export const CLI_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
6
+ function artifactPath(root, artifactId) {
7
+ if (!/^[a-f0-9-]{36}$/.test(artifactId))
8
+ throw new Error("invalid artifact id");
9
+ return join(resolve(root), `${artifactId}.artifact`);
10
+ }
11
+ function ensureRoot(root) {
12
+ const resolved = resolve(root);
13
+ mkdirSync(resolved, { recursive: true, mode: 0o700 });
14
+ return resolved;
15
+ }
16
+ export function createArtifact(root, content) {
17
+ const resolvedRoot = ensureRoot(root);
18
+ const artifactId = randomUUID();
19
+ const path = artifactPath(resolvedRoot, artifactId);
20
+ const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : content;
21
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
22
+ writeFileSync(path, bytes, { mode: 0o600, flag: "wx" });
23
+ chmodSync(path, 0o600);
24
+ writeFileSync(`${path}.sha256`, `${sha256}\n`, { mode: 0o600, flag: "wx" });
25
+ chmodSync(`${path}.sha256`, 0o600);
26
+ return { artifactId, sha256, path };
27
+ }
28
+ export function readArtifact(root, artifactId) {
29
+ const path = artifactPath(root, artifactId);
30
+ const metaPath = `${path}.sha256`;
31
+ if (!existsSync(path) || !existsSync(metaPath))
32
+ throw new Error("artifact not found");
33
+ if (lstatSync(path).isSymbolicLink() || lstatSync(metaPath).isSymbolicLink())
34
+ throw new Error("artifact is a symbolic link");
35
+ const content = readFileSync(path);
36
+ const actual = createHash("sha256").update(content).digest("hex");
37
+ const expected = readFileSync(metaPath, "utf8").trim();
38
+ if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected)
39
+ throw new Error("artifact sha256 verification failed");
40
+ return { content, sha256: actual };
41
+ }
42
+ export function isWithinRoot(root, candidate) {
43
+ const resolvedRoot = resolve(root);
44
+ const resolvedCandidate = resolve(candidate);
45
+ const relativePath = relative(resolvedRoot, resolvedCandidate);
46
+ // `relative` can return an ABSOLUTE path when the two paths share no common
47
+ // root — on Windows that is a different drive letter (D:\x vs C:\y) or a UNC
48
+ // path; on POSIX it always relativizes. Such a result must be treated as
49
+ // OUTSIDE, otherwise the containment check passes for a foreign path and the
50
+ // caller's walk-up loop below can never reach `root` (see assertSafeInputPath).
51
+ if (isAbsolute(relativePath))
52
+ return false;
53
+ return relativePath !== "" && relativePath !== ".." && !relativePath.startsWith("..") && !relativePath.startsWith("/") && !relativePath.startsWith("\\");
54
+ }
55
+ /**
56
+ * Resolve a WRITE target inside the configuration root (mirrors
57
+ * assertSafeInputPath's containment rule, but the file need not exist yet).
58
+ * `omc intent plan --plan-out` uses it so the written plan is readable by
59
+ * `omc intent apply`, which enforces the same root.
60
+ */
61
+ export function assertSafeOutputPath(root, candidate, label) {
62
+ const normalized = resolve(root, candidate);
63
+ if (!isWithinRoot(root, normalized))
64
+ throw new Error(`${label} must be inside the configuration root`);
65
+ const parent = resolve(normalized, "..");
66
+ if (!existsSync(parent))
67
+ throw new Error(`${label} parent directory does not exist`);
68
+ if (lstatSync(parent).isSymbolicLink())
69
+ throw new Error(`${label} cannot be inside a symbolic link`);
70
+ if (existsSync(normalized)) {
71
+ if (lstatSync(normalized).isSymbolicLink())
72
+ throw new Error(`${label} cannot be a symbolic link`);
73
+ if (!lstatSync(normalized).isFile())
74
+ throw new Error(`${label} must be a regular file`);
75
+ }
76
+ return normalized;
77
+ }
78
+ export function assertSafeInputPath(root, candidate, label) {
79
+ const resolvedRoot = resolve(root);
80
+ const normalized = resolve(root, candidate);
81
+ if (!isWithinRoot(root, normalized))
82
+ throw new Error(`${label} must be inside the configuration root`);
83
+ let current = normalized;
84
+ while (current !== resolvedRoot) {
85
+ try {
86
+ if (lstatSync(current).isSymbolicLink())
87
+ throw new Error(`${label} cannot be a symbolic link`);
88
+ }
89
+ catch (error) {
90
+ if (error instanceof Error && error.message === `${label} cannot be a symbolic link`)
91
+ throw error;
92
+ throw new Error(`${label} does not exist`);
93
+ }
94
+ const parent = resolve(current, "..");
95
+ // Terminating guard: `resolve(x, "..")` stops at the filesystem/drive root.
96
+ // If we reach it without hitting `resolvedRoot`, the candidate is not a
97
+ // descendant even though the containment check above said otherwise — refuse
98
+ // instead of looping forever (the Windows cross-drive hang, B1).
99
+ if (parent === current)
100
+ throw new Error(`${label} must be inside the configuration root`);
101
+ current = parent;
102
+ }
103
+ try {
104
+ const stat = lstatSync(normalized);
105
+ if (stat.isSymbolicLink())
106
+ throw new Error(`${label} cannot be a symbolic link`);
107
+ // A directory / FIFO / device would make the caller's readSync block (or
108
+ // misbehave); require a regular file so a bad path fails fast, never hangs.
109
+ if (!stat.isFile())
110
+ throw new Error(`${label} must be a regular file`);
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Error && (error.message === `${label} cannot be a symbolic link` || error.message === `${label} must be a regular file`))
114
+ throw error;
115
+ throw new Error(`${label} does not exist`);
116
+ }
117
+ return normalized;
118
+ }
119
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1,256 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { startFakeHttpServer } from "./fake-http-server.js";
6
+ /**
7
+ * Call-face eval (Spec 100 acceptance / Spec 107 baseline): a scripted
8
+ * reference agent completes real tasks using ONLY the omc CLI face
9
+ * (commands/describe/contract discovery + `omc call` with the two-step
10
+ * approval), one subprocess per invocation, against an HTTP-served fake.
11
+ * Metrics: task success, CLI invocations, error exits, envelope bytes
12
+ * (token proxy = bytes/4), approval rounds, business wire calls.
13
+ */
14
+ const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
15
+ const cli = join(packageRoot, "dist", "cli.js");
16
+ function ok(condition, message) {
17
+ if (!condition)
18
+ throw new Error(message);
19
+ }
20
+ async function makeAgent(home, metric) {
21
+ const env = { ...process.env, OMC_CONFIG_HOME: home };
22
+ const base = {
23
+ env: env,
24
+ async run(args) {
25
+ metric.cliInvocations += 1;
26
+ const kind = args[0] === "call" ? "callInvocations" : "discoveryInvocations";
27
+ metric[kind] += 1;
28
+ const argsJson = args.find((token) => token.startsWith("{"));
29
+ metric.requestBytes += argsJson ? Buffer.byteLength(argsJson) : args.join(" ").length;
30
+ return await new Promise((resolveRun, rejectRun) => {
31
+ const child = spawn(process.execPath, [cli, ...args], { cwd: home, env, stdio: ["pipe", "pipe", "pipe"] });
32
+ let stdout = "";
33
+ let stderr = "";
34
+ child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
35
+ child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8")));
36
+ child.on("error", rejectRun);
37
+ child.on("close", (code) => {
38
+ metric.responseBytes += Buffer.byteLength(stdout);
39
+ let envelope;
40
+ try {
41
+ envelope = JSON.parse(stdout);
42
+ }
43
+ catch (error) {
44
+ rejectRun(new Error(`stdout is not a single JSON envelope (${String(code)}): ${stdout.slice(0, 200)}\nstderr=${stderr.slice(0, 200)} ${error.message}`));
45
+ return;
46
+ }
47
+ if (code === 13)
48
+ metric.expectedApprovalExits += 1;
49
+ else if (code !== 0)
50
+ metric.errorExits += 1;
51
+ resolveRun({ code, envelope });
52
+ });
53
+ });
54
+ },
55
+ async write(args, options = {}) {
56
+ const tool = args.__tool;
57
+ const payload = { ...args };
58
+ delete payload.__tool;
59
+ const argsFlag = JSON.stringify(payload);
60
+ const callArgs = ["call", tool, "--env", "eval", "--args", argsFlag, ...(options.requestId ? ["--request-id", options.requestId] : [])];
61
+ const executed = await base.run(callArgs);
62
+ ok(executed.envelope.result?.status === "ok", `execute phase (${tool}): ${JSON.stringify(executed.envelope).slice(0, 260)}`);
63
+ return executed.envelope.result;
64
+ },
65
+ async read(tool, args) {
66
+ const result = await base.run(["call", tool, "--env", "eval", "--args", JSON.stringify(args)]);
67
+ ok(result.envelope.ok === true, `read ${tool}: ${JSON.stringify(result.envelope).slice(0, 220)}`);
68
+ return result.envelope.result;
69
+ },
70
+ async discovery(kind, value, flags = []) {
71
+ const args = value === undefined ? [kind, ...(kind === "commands" ? ["--json"] : []), ...flags] : [kind, value, ...flags];
72
+ const result = await base.run(args);
73
+ ok(result.envelope.ok === true, `discovery ${kind}: ${JSON.stringify(result.envelope).slice(0, 220)}`);
74
+ return result.envelope.result;
75
+ },
76
+ counts: () => metric,
77
+ };
78
+ return base;
79
+ }
80
+ function freshHome(url) {
81
+ const home = mkdtempSync(join(packageRoot, ".call-face-eval-"));
82
+ mkdirSync(home, { recursive: true });
83
+ writeFileSync(join(home, "omc.config.json"), JSON.stringify({ environments: { eval: { baseUrl: url, applicationCode: "test", isDefault: true, writable: true } } }));
84
+ writeFileSync(join(home, "credentials"), JSON.stringify({ eval: { username: "tester", password: "eval-face-password" } }));
85
+ return home;
86
+ }
87
+ export const CALL_FACE_TASKS = [
88
+ {
89
+ id: "usable-model-ladder",
90
+ description: "model + 3 fields + field publish + form draft/publish + verified readback (Spec 107 first slice, via call face)",
91
+ async run(agent) {
92
+ const inventory = await agent.discovery("commands");
93
+ ok(Array.isArray(inventory.tools) && inventory.tools.some((tool) => tool.name === "model.create"), "commands inventory missing model.create");
94
+ const described = await agent.discovery("describe", "field.create", ["--full"]);
95
+ ok(described.inputSchema?.properties !== undefined, "describe field.create --full lacks schema");
96
+ const contract = await agent.discovery("contract", "field-families", ["--full"]);
97
+ ok(contract.contract?.wire !== undefined, "contract field-families --full lacks wire");
98
+ await agent.write({ __tool: "model.create", name: "评测建模", code: "eval_m1" });
99
+ await agent.write({ __tool: "field.create", schemaCode: "eval_m1", code: "f_title", name: "标题", propertyType: "SHORT_TEXT" });
100
+ await agent.write({ __tool: "field.create", schemaCode: "eval_m1", code: "f_amount", name: "金额", propertyType: "NUMERICAL", options: { scale: 3 } });
101
+ await agent.write({ __tool: "field.create", schemaCode: "eval_m1", code: "f_when", name: "日期", propertyType: "DATE" });
102
+ await agent.write({ __tool: "field.publish", schemaCode: "eval_m1" });
103
+ await agent.write({ __tool: "form.draft", schemaCode: "eval_m1", sheetCode: "eval_m1" });
104
+ await agent.write({ __tool: "form.publish", schemaCode: "eval_m1", sheetCode: "eval_m1" });
105
+ const fields = await agent.read("field.list", { schemaCode: "eval_m1", published: true });
106
+ const codes = (fields.fields ?? []).map((field) => field.code);
107
+ ok(["f_title", "f_amount", "f_when"].every((code) => codes.includes(code)), `published fields missing: ${JSON.stringify(codes)}`);
108
+ const form = await agent.read("form.get", { schemaCode: "eval_m1", sheetCode: "eval_m1" });
109
+ ok(form.published === true, "form not published after ladder");
110
+ },
111
+ },
112
+ {
113
+ id: "rule-backfill-ladder",
114
+ description: "two models + DYNAMIC backfill rule + enable + trigger row + verified target readback (live-verified ticket 09 shape)",
115
+ async run(agent, server) {
116
+ await agent.discovery("contract", "B1-businessrule");
117
+ await agent.write({ __tool: "model.create", name: "回填源", code: "eval_rs" });
118
+ await agent.write({ __tool: "model.create", name: "回填目标", code: "eval_rt" });
119
+ server.fake.seedDataRow("eval_rt", { latestPeriod: "" }, "eval_rt_row");
120
+ await agent.write({
121
+ __tool: "rule.save",
122
+ schemaCode: "eval_rs",
123
+ ruleCode: "Create",
124
+ name: "回填",
125
+ nodeGraph: {
126
+ nodes: [
127
+ { nodeCode: "Start", nodeType: "START" },
128
+ {
129
+ nodeCode: "UPDATE_1",
130
+ nodeType: "UPDATE",
131
+ targetObjectCode: "eval_rt",
132
+ inputParam: "_input",
133
+ filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "project" }]], sheetConditions: [] },
134
+ dataActions: [{ targetDataItem: "latestPeriod", ruleValueType: "DYNAMIC", value: "", ruleActionType: "EQUALS", currentDataItemValue: "declarePeriod" }],
135
+ },
136
+ { nodeCode: "End", nodeType: "END" },
137
+ ],
138
+ routes: [{ preNode: "Start", postNode: "UPDATE_1" }, { preNode: "UPDATE_1", postNode: "End" }],
139
+ },
140
+ });
141
+ await agent.write({ __tool: "rule.enable", schemaCode: "eval_rs", ruleCode: "Create" });
142
+ await agent.write({ __tool: "data.save", schemaCode: "eval_rs", formData: { declarePeriod: "202612", project: { id: "eval_rt_row" } } });
143
+ const target = await agent.read("data.load", { schemaCode: "eval_rt", bizObjectId: "eval_rt_row" });
144
+ ok(target.data?.latestPeriod === "202612", `backfill=${String(target.data?.latestPeriod)}`);
145
+ },
146
+ },
147
+ {
148
+ id: "input-error-recovery",
149
+ description: "agent hits invalid-input (exit 10), recovers; requestId replay returns already-completed without a second create wire",
150
+ async run(agent, server) {
151
+ const invalid = await agent.run(["call", "model.create", "--env", "eval", "--args", "{not json"]);
152
+ ok(invalid.code === 10 && invalid.envelope.error?.code === "invalid-input", `expected invalid-input exit 10, got ${String(invalid.code)}`);
153
+ await agent.write({ __tool: "model.create", name: "幂等模型", code: "eval_idem" }, { requestId: "eval-idem-1" });
154
+ const createWiresBefore = server.fake.bodiesFor("/api/api/app/bizmodels/create").length;
155
+ ok(createWiresBefore === 1, `expected exactly one create wire, got ${String(createWiresBefore)}`);
156
+ const replay = await agent.run(["call", "model.create", "--env", "eval", "--args", JSON.stringify({ name: "幂等模型", code: "eval_idem" }), "--request-id", "eval-idem-1"]);
157
+ ok(replay.code === 0 && replay.envelope.result?.status === "ok" && replay.envelope.result?.replay === "already-completed", `replay=${JSON.stringify(replay.envelope).slice(0, 260)}`);
158
+ ok(server.fake.bodiesFor("/api/api/app/bizmodels/create").length === 1, "replay must not send a second create wire");
159
+ const diverged = await agent.run(["call", "model.create", "--env", "eval", "--args", JSON.stringify({ name: "幂等模型2", code: "eval_idem2" }), "--request-id", "eval-idem-1"]);
160
+ ok(diverged.code === 0 && diverged.envelope.result?.status === "ok", "same requestId with diverged args must execute as a new request");
161
+ ok(server.fake.bodiesFor("/api/api/app/bizmodels/create").length === 2, "diverged args must not replay the first request");
162
+ },
163
+ },
164
+ {
165
+ id: "read-exploration",
166
+ description: "discovery-driven read exploration: commands -> describe -> contract -> filtered data.list projection",
167
+ async run(agent, server) {
168
+ const inventory = await agent.discovery("commands");
169
+ const listTool = inventory.tools.find((tool) => tool.name === "data.list");
170
+ ok(listTool !== undefined, "data.list not discoverable");
171
+ await agent.discovery("describe", "data.list");
172
+ server.fake.seedField("eval_ro", "name", 0);
173
+ server.fake.seedDataRow("eval_ro", { name: "命中", state: "DRAFT" }, "eval_ro_hit");
174
+ server.fake.seedDataRow("eval_ro", { name: "未命中", state: "DRAFT" }, "eval_ro_miss");
175
+ await agent.write({ __tool: "listview.configure", schemaCode: "eval_ro", fieldCodes: ["name"] });
176
+ const listed = await agent.read("data.list", { schemaCode: "eval_ro", filter: { op: "eq", field: "name", value: "命中" } });
177
+ const items = (listed.items ?? []);
178
+ ok(items.length === 1 && items[0]?.fields?.name === "命中", `filtered projection wrong: ${JSON.stringify(items).slice(0, 160)}`);
179
+ },
180
+ },
181
+ ];
182
+ export async function runCallFaceEvals(options = {}) {
183
+ const tasks = options.only ? CALL_FACE_TASKS.filter((task) => task.id === options.only) : CALL_FACE_TASKS;
184
+ if (tasks.length === 0)
185
+ throw new Error(`no call-face task matches ${options.only}`);
186
+ const results = [];
187
+ for (const task of tasks) {
188
+ const server = await startFakeHttpServer();
189
+ const home = freshHome(server.url);
190
+ const metric = { cliInvocations: 0, discoveryInvocations: 0, callInvocations: 0, approvalRounds: 0, errorExits: 0, expectedApprovalExits: 0, requestBytes: 0, responseBytes: 0 };
191
+ const startedAt = Date.now();
192
+ let businessWireCalls = 0;
193
+ let failure;
194
+ try {
195
+ const agent = await makeAgent(home, metric);
196
+ await task.run(agent, server);
197
+ }
198
+ catch (error) {
199
+ failure = error instanceof Error ? error.message : String(error);
200
+ }
201
+ finally {
202
+ businessWireCalls = server.fake.state.callLog.filter((entry) => !entry.path.startsWith("/user/") && !entry.path.startsWith("/api/public/") && !entry.path.startsWith("/api/api/organization/")).length;
203
+ await server.close();
204
+ rmSync(home, { recursive: true, force: true });
205
+ }
206
+ results.push({
207
+ id: task.id,
208
+ passed: failure === undefined,
209
+ ...(failure ? { error: failure } : {}),
210
+ cliInvocations: metric.cliInvocations,
211
+ discoveryInvocations: metric.discoveryInvocations,
212
+ callInvocations: metric.callInvocations,
213
+ approvalRounds: metric.approvalRounds,
214
+ errorExits: metric.errorExits,
215
+ expectedApprovalExits: metric.expectedApprovalExits,
216
+ requestBytes: metric.requestBytes,
217
+ responseBytes: metric.responseBytes,
218
+ estimatedTokens: Math.round((metric.requestBytes + metric.responseBytes) / 4),
219
+ businessWireCalls,
220
+ wallClockMs: Date.now() - startedAt,
221
+ });
222
+ }
223
+ const sum = (pick) => results.reduce((total, entry) => total + pick(entry), 0);
224
+ return {
225
+ schema: "omc-call-face-eval-v1",
226
+ face: "call",
227
+ generatedAt: new Date().toISOString(),
228
+ tasks: results,
229
+ aggregate: {
230
+ tasksTotal: results.length,
231
+ tasksPassed: results.filter((entry) => entry.passed).length,
232
+ successRate: results.length === 0 ? 0 : Math.round((results.filter((entry) => entry.passed).length / results.length) * 10000) / 10000,
233
+ cliInvocations: sum((entry) => entry.cliInvocations),
234
+ discoveryInvocations: sum((entry) => entry.discoveryInvocations),
235
+ callInvocations: sum((entry) => entry.callInvocations),
236
+ approvalRounds: sum((entry) => entry.approvalRounds),
237
+ errorExits: sum((entry) => entry.errorExits),
238
+ requestBytes: sum((entry) => entry.requestBytes),
239
+ responseBytes: sum((entry) => entry.responseBytes),
240
+ estimatedTokens: sum((entry) => entry.estimatedTokens),
241
+ businessWireCalls: sum((entry) => entry.businessWireCalls),
242
+ },
243
+ };
244
+ }
245
+ export function formatCallFaceReport(report) {
246
+ const lines = [
247
+ `omc call-face eval — ${report.aggregate.tasksPassed}/${report.aggregate.tasksTotal} tasks passed (successRate=${report.aggregate.successRate})`,
248
+ ...report.tasks.map((task) => ` ${task.passed ? "PASS" : "FAIL"} ${task.id} — cli=${task.cliInvocations} discovery=${task.discoveryInvocations} approvals=${task.approvalRounds} errExits=${task.errorExits} wires=${task.businessWireCalls} tokens≈${task.estimatedTokens} ${task.wallClockMs}ms${task.error ? `\n ${task.error}` : ""}`),
249
+ `aggregate: cli=${report.aggregate.cliInvocations} discovery=${report.aggregate.discoveryInvocations} calls=${report.aggregate.callInvocations} approvals=${report.aggregate.approvalRounds} errExits=${report.aggregate.errorExits} wires=${report.aggregate.businessWireCalls} tokens≈${report.aggregate.estimatedTokens}`,
250
+ ];
251
+ return lines.join("\n");
252
+ }
253
+ export function callFaceCliPath() {
254
+ return resolve(cli);
255
+ }
256
+ //# sourceMappingURL=call-face-eval.js.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Context attribution (2026-09-26).
3
+ *
4
+ * `scripts/agent-context-report.mjs` measures a session's PEAK window pressure
5
+ * from the OpenCode session DB. This module answers the next question: what
6
+ * occupies that window? It is pure — no sqlite, no fs — so the report script and
7
+ * the test suite share ONE classifier instead of drifting apart.
8
+ *
9
+ * A session's distinct content splits into:
10
+ * - tool-result: what tools returned (the dominant share in real tasks)
11
+ * - reasoning: the model's thinking
12
+ * - assistant-text: the model's visible prose
13
+ * - step / other: framework bookkeeping (step-start/finish, patch, file)
14
+ *
15
+ * Tool results are further attributed: for `bash` parts the command is parsed
16
+ * for an OMC CLI invocation and its footprint grouped by
17
+ * `<subcommand> <target> [variant-flags]` — so the report can rank the faces
18
+ * (`describe --full`, bare `commands`, `capability --executable`, `help <topic>`)
19
+ * that actually fill the window, and a byte ceiling can be pinned to each.
20
+ *
21
+ * Byte counts, not tokens: the per-message token totals are authoritative for
22
+ * the window size, but the DB does not expose a token split per part, so the
23
+ * category/tool/command shares are proportions of distinct content bytes.
24
+ */
25
+ const EMPTY_CATEGORIES = {
26
+ "tool-result": 0,
27
+ reasoning: 0,
28
+ "assistant-text": 0,
29
+ step: 0,
30
+ other: 0,
31
+ };
32
+ /** Variant flags that change a face's size; positional/env flags are noise. */
33
+ const VARIANT_FLAGS = new Set(["--full", "--executable", "--json", "--compact", "--dry-run", "--all", "--schema-only"]);
34
+ const SUBCOMMAND_TARGETS = new Set(["call", "describe", "contract", "capability", "help", "intent", "bench", "artifact"]);
35
+ /**
36
+ * Match an OMC invocation: `node …/cli.js <rest>` or `omc <rest>`. Anchored to a
37
+ * command boundary (start of string, after `; & | (`, or after `npx`/`bunx`) so
38
+ * `... && node cli.js …` is found but `grep -rn omc .` is not.
39
+ */
40
+ const OMC_INVOCATION = new RegExp("(?:^|[;&|(]\\s*|\\b(?:npx|bunx)(?:\\.exe)?\\s+)" +
41
+ "(?:[A-Za-z_][A-Za-z0-9_]*=\\S+\\s+)*" +
42
+ "(?:(?:node|bun|npx|bunx)(?:\\.exe)?\\s+)?" +
43
+ "(?:[^\\s]*[\\/\\\\])?(?:omc(?:-cli)?|cli\\.js)" +
44
+ "\\s+(.+)$");
45
+ export function classifyPart(type) {
46
+ switch (type) {
47
+ case "tool":
48
+ return "tool-result";
49
+ case "reasoning":
50
+ return "reasoning";
51
+ case "text":
52
+ return "assistant-text";
53
+ case "step-start":
54
+ case "step-finish":
55
+ return "step";
56
+ default:
57
+ return "other";
58
+ }
59
+ }
60
+ export function parseOmcInvocation(command) {
61
+ const match = OMC_INVOCATION.exec(command.trim());
62
+ if (!match)
63
+ return undefined;
64
+ // Only the first command of a shell chain is a face; the rest is follow-up.
65
+ const segment = match[1].split(/\s*(?:&&|\|\||;|\|)\s*/)[0].trim();
66
+ const tokens = segment.split(/\s+/).filter(Boolean);
67
+ const subcommand = tokens[0] ?? "";
68
+ const flags = [];
69
+ for (const token of tokens.slice(1)) {
70
+ // Stop at the args payload: a JSON blob is data, not part of the face.
71
+ if (token === "--args" || token === "--args-file" || token === "-")
72
+ break;
73
+ const name = token.replace(/=.*$/, "");
74
+ if (token.startsWith("--") && VARIANT_FLAGS.has(name) && !flags.includes(name))
75
+ flags.push(name);
76
+ }
77
+ // `full:true` inside --args triggers the same oversized readback as --full.
78
+ if (!flags.includes("--full") && /["']?full["']?\s*:\s*true/.test(segment))
79
+ flags.push("--full");
80
+ const target = SUBCOMMAND_TARGETS.has(subcommand) ? tokens[1] ?? "" : "";
81
+ return { label: target ? `${subcommand} ${target}` : subcommand, flags };
82
+ }
83
+ export function attributeContext(parts) {
84
+ const categories = { ...EMPTY_CATEGORIES };
85
+ const tools = new Map();
86
+ const omc = new Map();
87
+ let totalBytes = 0;
88
+ for (const part of parts) {
89
+ const bytes = Number.isFinite(part.bytes) && part.bytes > 0 ? part.bytes : 0;
90
+ categories[classifyPart(part.type)] += bytes;
91
+ totalBytes += bytes;
92
+ if (part.type !== "tool")
93
+ continue;
94
+ const resultBytes = part.outputBytes ?? bytes;
95
+ const tool = part.tool ?? "unknown";
96
+ tools.set(tool, (tools.get(tool) ?? 0) + resultBytes);
97
+ if (tool === "bash" && part.toolCommand) {
98
+ const parsed = parseOmcInvocation(part.toolCommand);
99
+ if (parsed) {
100
+ const key = parsed.flags.length ? `${parsed.label} ${parsed.flags.join(" ")}` : parsed.label;
101
+ const entry = omc.get(key) ?? { label: parsed.label, flags: parsed.flags, calls: 0, bytes: 0 };
102
+ entry.calls += 1;
103
+ entry.bytes += resultBytes;
104
+ omc.set(key, entry);
105
+ }
106
+ }
107
+ }
108
+ return {
109
+ categories,
110
+ tools: [...tools.entries()].map(([tool, value]) => ({ tool, bytes: value })).sort((a, b) => b.bytes - a.bytes),
111
+ omcInvocations: [...omc.values()].map(({ label, flags, calls, bytes }) => ({ label, flags, calls, bytes })).sort((a, b) => b.bytes - a.bytes),
112
+ totalBytes,
113
+ estimatedTokens: Math.round(totalBytes / 4),
114
+ };
115
+ }
116
+ /** Combine per-session attributions into one (drives the baseline snapshot). */
117
+ export function mergeAttributions(attributions) {
118
+ const categories = { ...EMPTY_CATEGORIES };
119
+ const tools = new Map();
120
+ const omc = new Map();
121
+ let totalBytes = 0;
122
+ for (const attribution of attributions) {
123
+ for (const key of Object.keys(categories))
124
+ categories[key] += attribution.categories[key];
125
+ totalBytes += attribution.totalBytes;
126
+ for (const entry of attribution.tools)
127
+ tools.set(entry.tool, (tools.get(entry.tool) ?? 0) + entry.bytes);
128
+ for (const entry of attribution.omcInvocations) {
129
+ const key = entry.flags.length ? `${entry.label} ${entry.flags.join(" ")}` : entry.label;
130
+ const existing = omc.get(key) ?? { label: entry.label, flags: [...entry.flags], calls: 0, bytes: 0 };
131
+ existing.calls += entry.calls;
132
+ existing.bytes += entry.bytes;
133
+ omc.set(key, existing);
134
+ }
135
+ }
136
+ return {
137
+ categories,
138
+ tools: [...tools.entries()].map(([tool, value]) => ({ tool, bytes: value })).sort((a, b) => b.bytes - a.bytes),
139
+ omcInvocations: [...omc.values()].map(({ label, flags, calls, bytes }) => ({ label, flags, calls, bytes })).sort((a, b) => b.bytes - a.bytes),
140
+ totalBytes,
141
+ estimatedTokens: Math.round(totalBytes / 4),
142
+ };
143
+ }
144
+ export function categoryShares(attribution) {
145
+ const total = attribution.totalBytes || 1;
146
+ const shares = {};
147
+ for (const key of Object.keys(attribution.categories))
148
+ shares[key] = Number(((attribution.categories[key] / total) * 100).toFixed(2));
149
+ return shares;
150
+ }
151
+ //# sourceMappingURL=context-attribution.js.map