@stalfh233/omc-cli 0.0.0-stage → 0.4.10

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 +293 -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,127 @@
1
+ import { z } from "zod";
2
+ export class ToolRefusalError extends Error {
3
+ payload;
4
+ constructor(payload) {
5
+ super(String(payload["blocker"] ?? "tool refused"));
6
+ this.payload = payload;
7
+ }
8
+ }
9
+ /**
10
+ * The most informative message an error carries. `ToolRefusalError.message` is
11
+ * the BLOCKER code (its constructor sets it so), while the actionable human
12
+ * detail lives in `payload.message` — a refusal family whose detail is the
13
+ * platform shape (e.g. datarule options, bizservice config) must not lose it
14
+ * when an offline validator catches and re-serializes the error.
15
+ */
16
+ export function errorDetail(error) {
17
+ if (error instanceof ToolRefusalError)
18
+ return String(error.payload["message"] ?? error.message);
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ export class ToolUncertainError extends Error {
22
+ payload;
23
+ constructor(payload) {
24
+ super(String(payload["reason"] ?? "tool frozen uncertain"));
25
+ this.payload = payload;
26
+ }
27
+ }
28
+ export class ToolInputError extends Error {
29
+ payload;
30
+ constructor(payload) {
31
+ super(String(payload.message ?? "tool input is invalid"));
32
+ this.payload = payload;
33
+ }
34
+ }
35
+ /**
36
+ * Generic registration factory: the single place the untyped wire `args`
37
+ * (`Record<string, unknown>`, supplied by the CLI transport after schema
38
+ * validation) is re-typed to `z.infer<z.ZodObject<S>>`. Every lane handler
39
+ * receives fully typed args from its own inputSchema, so lane code never
40
+ * needs `args.x as T` assertions; the cast lives only at this boundary.
41
+ */
42
+ export function defineTool(definition) {
43
+ return definition;
44
+ }
45
+ /**
46
+ * Same object, different key per tool family (round 4, from the live key-name
47
+ * confusion: `configure_form` answered `sheetCode: Required` while the caller
48
+ * passed the model's `schemaCode`). When a required key is missing but a known
49
+ * synonym was supplied, say so instead of only "Required".
50
+ */
51
+ const KEY_SYNONYMS = Object.freeze({
52
+ sheetCode: ["schemaCode", "code"],
53
+ workflowCode: ["code", "schemaCode"],
54
+ schemaCode: ["code", "sheetCode", "workflowCode"],
55
+ code: ["schemaCode", "sheetCode", "workflowCode"],
56
+ ruleCode: ["code"],
57
+ });
58
+ /**
59
+ * Per-tool input aliases: a synonym the caller may send INSTEAD of the canonical
60
+ * key. Applied in `parseToolInput` BEFORE validation (fill canonical from an
61
+ * alias only when the canonical key is absent). Deliberately per-tool and NOT
62
+ * global: `code`'s synonyms (`schemaCode`/`workflowCode`) are NOT interchangeable
63
+ * on every tool (e.g. `workflow.create`'s `code` is the NEW workflow, while
64
+ * `workflowCode` is a different object), so a global mapping would silently
65
+ * misroute arguments. Only add an entry when the two keys name the SAME object.
66
+ *
67
+ * Round 10 (sandbox 47): an agent sent `workflowCode` to `workflow.update` and got
68
+ * `code: Required`; the read tools take `workflowCode`, the write tools take
69
+ * `code`, and they name the same workflow.
70
+ */
71
+ const TOOL_INPUT_ALIASES = Object.freeze({
72
+ "workflow.create": Object.freeze({ code: ["workflowCode"] }),
73
+ "workflow.update": Object.freeze({ code: ["workflowCode"] }),
74
+ "workflow.publish": Object.freeze({ code: ["workflowCode"] }),
75
+ "workflow.delete": Object.freeze({ code: ["workflowCode"] }),
76
+ // field family: the platform's own column key is `propertyCode`; the tool's is
77
+ // `code`. Same field, so the alias is safe either direction.
78
+ "field.create": Object.freeze({ code: ["propertyCode"] }),
79
+ "field.update": Object.freeze({ code: ["propertyCode"] }),
80
+ "field.delete": Object.freeze({ code: ["propertyCode"] }),
81
+ // data.load's row id: callers habitually send `id` (bench67 wasted a round on
82
+ // `bizObjectId: Required`). On this tool `id` unambiguously names the same
83
+ // business object, so fill the canonical key rather than fail.
84
+ "data.load": Object.freeze({ bizObjectId: ["id"] }),
85
+ });
86
+ function applyInputAliases(tool, input) {
87
+ const aliases = TOOL_INPUT_ALIASES[tool.name];
88
+ if (!aliases)
89
+ return input;
90
+ let normalized;
91
+ for (const [canonical, synonyms] of Object.entries(aliases)) {
92
+ if (input[canonical] !== undefined)
93
+ continue;
94
+ const hit = synonyms.find((synonym) => input[synonym] !== undefined);
95
+ if (hit === undefined)
96
+ continue;
97
+ normalized ??= { ...input };
98
+ normalized[canonical] = input[hit];
99
+ }
100
+ return normalized ?? input;
101
+ }
102
+ function keyRenameHint(issuePath, input) {
103
+ const key = typeof issuePath[0] === "string" ? issuePath[0] : undefined;
104
+ if (!key)
105
+ return undefined;
106
+ const synonyms = (KEY_SYNONYMS[key] ?? []).filter((candidate) => input[candidate] !== undefined);
107
+ if (synonyms.length === 0)
108
+ return undefined;
109
+ // Round 10: list ALL synonyms the caller actually sent, not just the first — an
110
+ // agent that sent BOTH schemaCode and workflowCode for a `code` slot was told
111
+ // "你传的是 schemaCode", which pointed at the wrong key.
112
+ return `(该对象在本工具里的键是 ${key},你传的是 ${synonyms.join("/")};键名对照见 omc-tool-selection 的 Key-name table)`;
113
+ }
114
+ export function parseToolInput(tool, input) {
115
+ const normalized = applyInputAliases(tool, input);
116
+ const result = z.object(tool.inputSchema).safeParse(normalized);
117
+ if (result.success)
118
+ return result.data;
119
+ throw new ToolInputError({
120
+ status: "error",
121
+ blocker: "tool-input-invalid",
122
+ tool: tool.name,
123
+ message: result.error.issues.map((issue) => `${issue.path.join(".") || "args"}: ${issue.message}${keyRenameHint(issue.path, normalized) ?? ""}`).join("; "),
124
+ issues: result.error.issues.map((issue) => ({ path: issue.path, code: issue.code, message: issue.message, ...(keyRenameHint(issue.path, normalized) ? { hint: keyRenameHint(issue.path, normalized) } : {}) })),
125
+ });
126
+ }
127
+ //# sourceMappingURL=tool-types.js.map
package/dist/tools.js ADDED
@@ -0,0 +1,59 @@
1
+ import { loadCoverageManifest } from "./coverage.js";
2
+ import { makeToolContext } from "./context.js";
3
+ import { registerMetaLane } from "./lanes/meta.js";
4
+ import { registerEnvironmentLane } from "./lanes/environment.js";
5
+ import { registerAppLane } from "./lanes/app.js";
6
+ import { registerDictionaryLane } from "./lanes/dictionary.js";
7
+ import { registerModelLane } from "./lanes/model.js";
8
+ import { registerFieldLane } from "./lanes/field.js";
9
+ import { registerDataRuleLane } from "./lanes/datarule.js";
10
+ import { registerFormLane } from "./lanes/form.js";
11
+ import { registerBusinessRuleLane } from "./lanes/businessrule.js";
12
+ import { registerRuleLifecycleLane } from "./lanes/rule-lifecycle.js";
13
+ import { registerRuntimeDataLane } from "./lanes/runtime-data.js";
14
+ import { registerWorkflowLane } from "./lanes/workflow.js";
15
+ import { registerOnlineJsLane } from "./lanes/online-js.js";
16
+ import { registerB2Lane } from "./lanes/b2-registration.js";
17
+ import { registerListViewLane } from "./lanes/list-view.js";
18
+ import { registerReportLane } from "./lanes/report.js";
19
+ import { registerApplyChangesLane } from "./lanes/apply-changes.js";
20
+ import { executionMetadataFor } from "./execution-metadata.js";
21
+ export { ToolRefusalError, ToolUncertainError } from "./tool-types.js";
22
+ export { makeToolContext };
23
+ /**
24
+ * Composition root: assemble the governed tool surface from one adapter
25
+ * module per configuration lane. The lane modules own their tool
26
+ * registration, wire DTO construction, readback decoding, and lane/scope
27
+ * declarations; the shared governance pipelines live in ./read.ts / ./write.ts
28
+ * / ./package.ts (planning in ./plan.ts, wiring in ./context.ts).
29
+ */
30
+ export function buildToolRegistry() {
31
+ const tools = [];
32
+ registerMetaLane(tools);
33
+ registerEnvironmentLane(tools);
34
+ registerAppLane(tools);
35
+ registerDictionaryLane(tools);
36
+ registerModelLane(tools);
37
+ registerFieldLane(tools);
38
+ registerDataRuleLane(tools);
39
+ registerFormLane(tools);
40
+ registerBusinessRuleLane(tools);
41
+ registerRuleLifecycleLane(tools);
42
+ registerRuntimeDataLane(tools);
43
+ registerWorkflowLane(tools);
44
+ registerOnlineJsLane(tools);
45
+ registerB2Lane(tools);
46
+ registerListViewLane(tools);
47
+ registerReportLane(tools);
48
+ // LAST: apply_changes looks up sibling write tools at call time, so it must
49
+ // capture the fully-populated registry array.
50
+ registerApplyChangesLane(tools);
51
+ for (const tool of tools) {
52
+ const metadata = executionMetadataFor(tool);
53
+ if (metadata)
54
+ tool.executionMetadata = metadata;
55
+ }
56
+ return tools;
57
+ }
58
+ export { loadCoverageManifest };
59
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,197 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, resolve } from "node:path";
4
+ let pending;
5
+ function logPath() {
6
+ const explicit = process.env.OMC_USAGE_LOG;
7
+ if (explicit)
8
+ return resolve(explicit);
9
+ const projectEnv = resolve(process.cwd(), ".omc-env");
10
+ return existsSync(projectEnv) ? resolve(process.cwd(), ".omc", "usage.jsonl") : undefined;
11
+ }
12
+ function commandOf(argv) {
13
+ const values = [];
14
+ for (let index = 2; index < argv.length && values.length < 2; index += 1) {
15
+ const token = argv[index];
16
+ if (token.startsWith("--")) {
17
+ if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path"].includes(token))
18
+ index += 1;
19
+ continue;
20
+ }
21
+ values.push(token);
22
+ }
23
+ return values.join(" ") || "(none)";
24
+ }
25
+ function facesOf(argv) {
26
+ // `--json` is included: `commands` and `commands --json` differ by ~48 KB
27
+ // (bounded index vs full catalogue) but `commandOf` drops every flag, so
28
+ // without this they were indistinguishable in the log and byte attribution
29
+ // was ambiguous (round 10).
30
+ const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--out", "--quiet"];
31
+ return faces.filter((face) => argv.includes(face));
32
+ }
33
+ const safeArgumentKeys = new Set(["schemaCode", "propertyCode", "code", "workflowCode", "ruleCode", "sheetCode", "id", "dataRuleType", "propertyType", "published", "handler"]);
34
+ function summarizeArgs(argv) {
35
+ const summary = { argKeys: [] };
36
+ const argsIndex = argv.indexOf("--args");
37
+ if (argsIndex >= 0) {
38
+ try {
39
+ const parsed = JSON.parse(argv[argsIndex + 1] ?? "{}");
40
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
41
+ const keys = Object.keys(parsed).sort();
42
+ summary.argKeys = keys;
43
+ for (const key of keys)
44
+ if (safeArgumentKeys.has(key)) {
45
+ const value = parsed[key];
46
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
47
+ summary[key] = value;
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ summary.argKeys = ["<invalid-json>"];
53
+ }
54
+ }
55
+ const envIndex = argv.findIndex((token) => token === "--env" || token === "--environment");
56
+ if (envIndex >= 0 && argv[envIndex + 1])
57
+ summary.environment = argv[envIndex + 1];
58
+ const sectionIndex = argv.indexOf("--section");
59
+ if (sectionIndex >= 0 && argv[sectionIndex + 1])
60
+ summary.section = argv[sectionIndex + 1];
61
+ if (argv.includes("--sections"))
62
+ summary.sections = true;
63
+ return summary;
64
+ }
65
+ export function beginUsage(argv) {
66
+ const path = logPath();
67
+ if (!path)
68
+ return;
69
+ const raw = argv.slice(2).join("\u0000");
70
+ const argsDigest = createHash("sha256").update(raw).digest("hex");
71
+ pending = {
72
+ id: createHash("sha256").update(`${Date.now()}\u0000${raw}`).digest("hex").slice(0, 16),
73
+ startedAt: new Date().toISOString(),
74
+ startedMs: Date.now(),
75
+ command: commandOf(argv),
76
+ argsDigest,
77
+ argSummary: summarizeArgs(argv),
78
+ faces: facesOf(argv),
79
+ stdoutBytes: 0,
80
+ fileBytes: 0,
81
+ status: "no-envelope",
82
+ };
83
+ }
84
+ export function recordUsageOutput(value, stdoutBytes, fileBytes = 0) {
85
+ if (!pending)
86
+ return;
87
+ const envelope = value && typeof value === "object" ? value : {};
88
+ const error = envelope.error && typeof envelope.error === "object" ? envelope.error : undefined;
89
+ pending.stdoutBytes = stdoutBytes;
90
+ pending.fileBytes = fileBytes;
91
+ pending.status = String(envelope.status ?? "unknown");
92
+ if (typeof error?.code === "string")
93
+ pending.errorCode = error.code;
94
+ }
95
+ export function completeUsage(exitCode) {
96
+ if (!pending)
97
+ return;
98
+ const current = pending;
99
+ pending = undefined;
100
+ const path = logPath();
101
+ if (!path)
102
+ return;
103
+ mkdirSync(dirname(path), { recursive: true });
104
+ const record = {
105
+ kind: "invocation",
106
+ id: current.id,
107
+ startedAt: current.startedAt,
108
+ command: current.command,
109
+ argsDigest: current.argsDigest,
110
+ argSummary: current.argSummary,
111
+ faces: current.faces,
112
+ stdoutBytes: current.stdoutBytes,
113
+ fileBytes: current.fileBytes,
114
+ durationMs: Date.now() - current.startedMs,
115
+ status: current.status,
116
+ exitCode,
117
+ ...(current.errorCode ? { errorCode: current.errorCode } : {}),
118
+ };
119
+ appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
120
+ }
121
+ function readUsage(path) {
122
+ if (!existsSync(path))
123
+ return [];
124
+ return readFileSync(path, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
125
+ try {
126
+ const value = JSON.parse(line);
127
+ return value && typeof value === "object" ? [value] : [];
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ });
133
+ }
134
+ export function usageReport(pathArg) {
135
+ const path = pathArg ? resolve(pathArg) : logPath() ?? resolve(process.cwd(), ".omc", "usage.jsonl");
136
+ const records = readUsage(path);
137
+ const calls = records.filter((record) => record.kind === "invocation");
138
+ const commandCounts = new Map();
139
+ let stdoutBytes = 0;
140
+ let fileBytes = 0;
141
+ let fullCalls = 0;
142
+ for (const record of calls) {
143
+ const command = String(record.command ?? "(unknown)");
144
+ commandCounts.set(command, (commandCounts.get(command) ?? 0) + 1);
145
+ stdoutBytes += Number(record.stdoutBytes ?? 0);
146
+ fileBytes += Number(record.fileBytes ?? 0);
147
+ if (Array.isArray(record.faces) && record.faces.includes("--full"))
148
+ fullCalls += 1;
149
+ }
150
+ const repeated = [...commandCounts.entries()].filter(([, count]) => count > 1).map(([command, count]) => ({ command, count }));
151
+ const exactCounts = new Map();
152
+ const createdCodes = new Set();
153
+ const referencedCodes = new Set();
154
+ for (const record of calls) {
155
+ const summary = record.argSummary && typeof record.argSummary === "object" ? record.argSummary : {};
156
+ const command = String(record.command ?? "");
157
+ const digest = `${command}:${String(record.argsDigest ?? "")}`;
158
+ exactCounts.set(digest, (exactCounts.get(digest) ?? 0) + 1);
159
+ if (command === "call model.create" && typeof summary.code === "string")
160
+ createdCodes.add(summary.code);
161
+ for (const key of ["schemaCode", "workflowCode"])
162
+ if (typeof summary[key] === "string")
163
+ referencedCodes.add(summary[key]);
164
+ }
165
+ const exactRepeated = [...exactCounts.entries()].filter(([, count]) => count > 1).map(([digest, count]) => ({ digest: digest.slice(-16), count }));
166
+ const unseenTargetReferences = [...referencedCodes].filter((code) => !createdCodes.has(code));
167
+ // Round 4: attribute large reads. The complex benchmark showed `artifact get`
168
+ // at 56% of stdout; without this the next session cannot see the whale.
169
+ const artifactBytes = calls.filter((record) => String(record.command).startsWith("artifact get")).reduce((sum, record) => sum + Number(record.stdoutBytes ?? 0), 0);
170
+ const largestReads = calls
171
+ .filter((record) => Number(record.stdoutBytes ?? 0) >= 8192)
172
+ .map((record) => ({ command: String(record.command ?? ""), bytes: Number(record.stdoutBytes ?? 0), summary: record.argSummary ?? {} }))
173
+ .sort((left, right) => right.bytes - left.bytes)
174
+ .slice(0, 8);
175
+ const violations = [
176
+ ...(fullCalls > 3 ? [{ code: "batch-full-prefetch", message: `${fullCalls} calls used --full; prefer next/recipe/default describe` }] : []),
177
+ ...(calls.some((record) => String(record.command).startsWith("commands") && !Array.isArray(record.faces)) ? [{ code: "broad-command-catalog", message: "full commands inventory was requested" }] : []),
178
+ ...(artifactBytes > 32768 ? [{ code: "large-artifact-read", message: `artifact get pulled ${artifactBytes} B; prefer --summary/--keys/--jq or the projected workflow summary` }] : []),
179
+ ];
180
+ return {
181
+ status: "ok",
182
+ path,
183
+ calls: calls.length,
184
+ stdoutBytes,
185
+ fileBytes,
186
+ fullCalls,
187
+ repeated,
188
+ exactRepeated,
189
+ createdCodes: [...createdCodes],
190
+ unseenTargetReferences,
191
+ artifactBytes,
192
+ largestReads,
193
+ violations,
194
+ records,
195
+ };
196
+ }
197
+ //# sourceMappingURL=usage-log.js.map
package/dist/wire.js ADDED
@@ -0,0 +1,287 @@
1
+ import { CloudPivotRequestTimeoutError } from "./session.js";
2
+ import { ENDPOINTS } from "./endpoints.js";
3
+ /**
4
+ * Generic governed wire driver. Decodes the CloudPivot envelope
5
+ * ({errcode, errmsg, data}) with Void-envelope semantics, and classifies
6
+ * failures: business errors are definitive refusals; transport failures are
7
+ * UNCERTAIN — callers must freeze, never assume success.
8
+ */
9
+ export class WireError extends Error {
10
+ kind;
11
+ errcode;
12
+ diagnostic;
13
+ constructor(kind, message, errcode,
14
+ /** Round-2 T-06: diagnosable context for malformed/http-status envelopes. */
15
+ diagnostic) {
16
+ super(message);
17
+ this.kind = kind;
18
+ this.errcode = errcode;
19
+ this.diagnostic = diagnostic;
20
+ }
21
+ }
22
+ /** Redact common secret keys and truncate — the malformed-envelope preview must be safe to log. */
23
+ function redactedBodyPreview(body, limit = 200) {
24
+ let text;
25
+ try {
26
+ text = typeof body === "string" ? body : JSON.stringify(body);
27
+ }
28
+ catch {
29
+ return "<unserializable>";
30
+ }
31
+ if (text === undefined)
32
+ return "<empty>";
33
+ const redacted = text.replace(/("(?:token|password|passwd|secret|authorization|access[_-]?token|refresh[_-]?token|code|privateKey|clientSecret)"\s*:\s*)"[^"]*"/gi, '$1"***"');
34
+ return redacted.length > limit ? `${redacted.slice(0, limit)}…` : redacted;
35
+ }
36
+ /**
37
+ * Per-session read cache for DESIGN-TIME metadata GETs only (ticket 16).
38
+ *
39
+ * Cached faces (sheetGet / fieldList / applicationTree) change only through
40
+ * this session's design-surface POSTs, so every write invalidates exactly the
41
+ * entries its wire family can mutate — scoped by the schema code carried in
42
+ * the POST body. Rule-face and list-view query-face POSTs mutate none of the
43
+ * cached faces and therefore invalidate nothing. Runtime-data / workflow /
44
+ * auth POSTs never invalidate either: they cannot change a form schema, field
45
+ * metadata, or the app directory tree. Unknown /api/api/app/ writes still
46
+ * clear everything (fail safe). A 60s TTL bounds staleness against
47
+ * out-of-band mutations (e.g. a human designer in the same environment);
48
+ * failures are never cached; hits return a deep clone so callers cannot
49
+ * poison the cache by mutating the payload.
50
+ */
51
+ const READ_CACHE_PATHS = new Set([ENDPOINTS.sheetGet, ENDPOINTS.fieldList, ENDPOINTS.applicationTree]);
52
+ export const WIRE_READ_CACHE_TTL_MS = 60_000;
53
+ const readCacheBySession = new WeakMap();
54
+ let totalHits = 0;
55
+ let totalMisses = 0;
56
+ let clock = () => Date.now();
57
+ /** Aggregated since the last reset (bench: one reset per harness). */
58
+ export function wireReadCacheStats() {
59
+ return { hits: totalHits, misses: totalMisses };
60
+ }
61
+ export function resetWireReadCacheStats() {
62
+ totalHits = 0;
63
+ totalMisses = 0;
64
+ }
65
+ /** Test seam for TTL expiry. */
66
+ export function __setWireCacheClockForTests(override) {
67
+ clock = override ?? (() => Date.now());
68
+ }
69
+ function cacheKey(path, query) {
70
+ const sorted = Object.keys(query)
71
+ .sort()
72
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key] ?? "")}`)
73
+ .join("&");
74
+ return sorted ? `${path}?${sorted}` : path;
75
+ }
76
+ function readCacheStateOf(session) {
77
+ let state = readCacheBySession.get(session);
78
+ if (!state) {
79
+ state = { entries: new Map() };
80
+ readCacheBySession.set(session, state);
81
+ }
82
+ return state;
83
+ }
84
+ /** Schema codes referenced by a write body (schemaCode / bizSchemaCode, plus BizModel array items' code). */
85
+ function schemaCodesFromBody(body) {
86
+ const codes = new Set();
87
+ const visit = (value) => {
88
+ if (Array.isArray(value)) {
89
+ for (const item of value) {
90
+ visit(item);
91
+ if (item && typeof item === "object" && item.type === "BizModel") {
92
+ const code = item.code;
93
+ if (typeof code === "string" && code)
94
+ codes.add(code);
95
+ }
96
+ }
97
+ return;
98
+ }
99
+ if (value && typeof value === "object") {
100
+ const record = value;
101
+ for (const key of ["schemaCode", "bizSchemaCode"]) {
102
+ const code = record[key];
103
+ if (typeof code === "string" && code)
104
+ codes.add(code);
105
+ }
106
+ }
107
+ };
108
+ visit(body);
109
+ return [...codes];
110
+ }
111
+ function invalidateReadCache(session, call) {
112
+ const state = readCacheBySession.get(session);
113
+ if (!state || state.entries.size === 0)
114
+ return;
115
+ const dropSchemaScoped = (paths, codes) => {
116
+ for (const [key, entry] of state.entries) {
117
+ if (!paths.includes(entry.result.path))
118
+ continue;
119
+ if (codes.length === 0 || codes.includes(entry.query.schemaCode ?? ""))
120
+ state.entries.delete(key);
121
+ }
122
+ };
123
+ const dropAll = (paths) => {
124
+ for (const [key, entry] of state.entries)
125
+ if (paths.includes(entry.result.path))
126
+ state.entries.delete(key);
127
+ };
128
+ const fieldPaths = [ENDPOINTS.fieldCreate, ENDPOINTS.fieldUpdate, ENDPOINTS.fieldPublish, ENDPOINTS.fieldDelete, ENDPOINTS.fieldForceDelete];
129
+ // Rule-face and list-view query-face POSTs never mutate the cached faces:
130
+ // rule metadata and query views are stored independently of the bizsheet
131
+ // layout JSONs, bizproperty field metadata, and the app directory tree
132
+ // (live evidence: ticket 14 rule save→enable→rule.get round-trip leaves the
133
+ // form readback unchanged). Everything else under /api/api/app/ stays in the
134
+ // fail-safe clear-all branch below.
135
+ const nonCachingDesignWrites = new Set([
136
+ ENDPOINTS.businessRuleCreate,
137
+ ENDPOINTS.businessRuleUpdate,
138
+ ENDPOINTS.businessRulePublish,
139
+ ENDPOINTS.businessRuleEnable,
140
+ ENDPOINTS.businessRuleDelete,
141
+ ENDPOINTS.listQueryCreate,
142
+ ENDPOINTS.listQueryUpdate,
143
+ ENDPOINTS.listQueryDelete,
144
+ // Field-level data rules live in their own metadata table and never
145
+ // rewrite the bizsheet layout, bizproperty field rows, or app tree
146
+ // (BizDataRuleController writes h_biz_data_rule only).
147
+ ENDPOINTS.dataRuleCreate,
148
+ ENDPOINTS.dataRuleUpdate,
149
+ ENDPOINTS.dataRuleDelete,
150
+ ]);
151
+ const path = call.path;
152
+ if (fieldPaths.includes(path)) {
153
+ // Field writes change the field faces; sheet metadata is dropped for the
154
+ // same schema as a conservative guard.
155
+ dropSchemaScoped([ENDPOINTS.fieldList, ENDPOINTS.sheetGet], schemaCodesFromBody(call.body));
156
+ }
157
+ else if (path === ENDPOINTS.sheetCreate || path === ENDPOINTS.sheetUpdate || path === ENDPOINTS.sheetPublish) {
158
+ dropSchemaScoped([ENDPOINTS.sheetGet], schemaCodesFromBody(call.body));
159
+ dropAll([ENDPOINTS.applicationTree]);
160
+ }
161
+ else if (path === ENDPOINTS.modelCreate || path === ENDPOINTS.appPackagePhysicallyDelete) {
162
+ // Model create/delete reshapes the app tree; cascade delete also removes
163
+ // the model's fields and forms.
164
+ dropSchemaScoped([ENDPOINTS.fieldList, ENDPOINTS.sheetGet], schemaCodesFromBody(call.body));
165
+ dropAll([ENDPOINTS.applicationTree]);
166
+ }
167
+ else if (nonCachingDesignWrites.has(path)) {
168
+ // Rule/query writes never mutate the cached faces — nothing to drop.
169
+ }
170
+ else if (path.startsWith("/api/api/app/")) {
171
+ // Unknown design-surface write — fail safe, drop everything cached.
172
+ state.entries.clear();
173
+ }
174
+ // runtime/workflow/auth/programming writes never mutate the cached faces.
175
+ }
176
+ export async function wireCall(session, call) {
177
+ const isPost = call.method === "POST";
178
+ if (isPost)
179
+ invalidateReadCache(session, call);
180
+ const cacheable = !isPost && READ_CACHE_PATHS.has(call.path);
181
+ const state = cacheable ? readCacheStateOf(session) : undefined;
182
+ const key = state ? cacheKey(call.path, call.query ?? {}) : "";
183
+ if (state) {
184
+ const entry = state.entries.get(key);
185
+ if (entry && clock() - entry.storedAt < WIRE_READ_CACHE_TTL_MS) {
186
+ totalHits += 1;
187
+ return structuredClone(entry.result);
188
+ }
189
+ if (entry)
190
+ state.entries.delete(key);
191
+ totalMisses += 1;
192
+ }
193
+ const response = await session.request({
194
+ path: call.path,
195
+ method: call.method ?? "GET",
196
+ ...(call.query ? { query: call.query } : {}),
197
+ ...(call.body === undefined ? {} : { body: call.body }),
198
+ }).catch((error) => {
199
+ // A timeout leaves the outcome unknown — classify it as a transport
200
+ // failure so the governance layer freezes instead of assuming success.
201
+ if (error instanceof CloudPivotRequestTimeoutError)
202
+ throw new WireError("transport", error.message);
203
+ throw error;
204
+ });
205
+ if (response.status < 200 || response.status >= 300)
206
+ throw new WireError("http-status", `HTTP ${response.status} for ${call.path} (${call.method ?? "GET"}); body: ${redactedBodyPreview(response.body)}`, undefined, {
207
+ httpStatus: response.status,
208
+ bodyPreview: redactedBodyPreview(response.body),
209
+ });
210
+ const body = response.body;
211
+ if (!body || typeof body !== "object")
212
+ throw new WireError("malformed-envelope", `non-object envelope for ${call.path} (${call.method ?? "GET"}); body: ${redactedBodyPreview(body)}`, undefined, { bodyPreview: redactedBodyPreview(body) });
213
+ const envelope = body;
214
+ if (typeof envelope.errcode !== "number")
215
+ // Round-2 T-06: a non-numeric errcode previously gave no clue WHICH
216
+ // business condition the platform meant (exists / validation / error). The
217
+ // message now carries the method, endpoint, and a redacted body preview.
218
+ throw new WireError("malformed-envelope", `envelope for ${call.path} (${call.method ?? "GET"}) has no numeric errcode; body: ${redactedBodyPreview(body)}`, undefined, { httpStatus: response.status, bodyPreview: redactedBodyPreview(body) });
219
+ if (envelope.errcode !== 0) {
220
+ const message = typeof envelope.errmsg === "string" ? envelope.errmsg : "CloudPivot rejected the request";
221
+ throw new WireError("business", `errcode ${envelope.errcode} for ${call.path}: ${message}`, envelope.errcode);
222
+ }
223
+ const result = {
224
+ path: call.path,
225
+ httpStatus: response.status,
226
+ errcode: envelope.errcode,
227
+ data: envelope.data ?? null,
228
+ readbackFingerprint: "",
229
+ };
230
+ if (state)
231
+ state.entries.set(key, { result: structuredClone(result), query: call.query ?? {}, storedAt: clock() });
232
+ return result;
233
+ }
234
+ /** List rows from the standard list envelope (array or {data:[]}). */
235
+ export function wireRows(value) {
236
+ const rows = Array.isArray(value)
237
+ ? value
238
+ : value && typeof value === "object" && Array.isArray(value.data)
239
+ ? value.data
240
+ : undefined;
241
+ if (!rows || !rows.every((row) => row && typeof row === "object"))
242
+ throw new WireError("malformed-envelope", "expected row array in envelope data");
243
+ return rows;
244
+ }
245
+ /**
246
+ * Ticket 106 — binary POST download (the bizservice export answers a ZIP
247
+ * stream, not an envelope). Non-2xx is an http-status WireError (UNCERTAIN),
248
+ * same classification as the envelope path.
249
+ */
250
+ export async function wireDownloadBuffer(session, call) {
251
+ const response = await session
252
+ .request({ path: call.path, method: "POST", ...(call.body === undefined ? {} : { body: call.body }), responseType: "buffer" })
253
+ .catch((error) => {
254
+ if (error instanceof CloudPivotRequestTimeoutError)
255
+ throw new WireError("transport", error.message);
256
+ throw error;
257
+ });
258
+ if (response.status < 200 || response.status >= 300)
259
+ throw new WireError("http-status", `HTTP ${response.status} for ${call.path} (POST)`, undefined, { httpStatus: response.status });
260
+ if (!Buffer.isBuffer(response.body) || response.body.length === 0)
261
+ throw new WireError("malformed-envelope", `empty or non-buffer body for ${call.path} (POST)`);
262
+ return response.body;
263
+ }
264
+ /**
265
+ * Ticket 106 — multipart file upload (POST /runtime/query/upload_file). The
266
+ * endpoint answers a PLAIN STRING refId (not an envelope;
267
+ * QueryRuntimeController.java:2414-2428). Each refId is ONE-SHOT: the
268
+ * consuming check/import download removes the refId→filename cache entry
269
+ * (FileOperateHelper2.java:43-57), so every check/import call uploads its own
270
+ * fresh copy.
271
+ */
272
+ export async function wireUploadFile(session, path, filename, content) {
273
+ const response = await session
274
+ .request({ path, method: "POST", multipart: { filename, contentType: "application/zip", content }, responseType: "buffer" })
275
+ .catch((error) => {
276
+ if (error instanceof CloudPivotRequestTimeoutError)
277
+ throw new WireError("transport", error.message);
278
+ throw error;
279
+ });
280
+ if (response.status < 200 || response.status >= 300)
281
+ throw new WireError("http-status", `HTTP ${response.status} for ${path} (POST upload)`, undefined, { httpStatus: response.status });
282
+ const refId = (Buffer.isBuffer(response.body) ? response.body.toString("utf8") : String(response.body ?? "")).trim();
283
+ if (!refId)
284
+ throw new WireError("malformed-envelope", `empty upload refId for ${path}`);
285
+ return refId;
286
+ }
287
+ //# sourceMappingURL=wire.js.map