@stalfh233/omc-cli 0.4.102 → 0.5.1

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 (225) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +359 -359
  3. package/coverage/m1-coverage-manifest-v1.json +1414 -1414
  4. package/dist/approval-flow.js +0 -0
  5. package/dist/approval-token.js +102 -0
  6. package/dist/args.js +0 -0
  7. package/dist/artifacts.js +0 -0
  8. package/dist/batch-call.js +0 -0
  9. package/dist/bench/call-face-eval.js +0 -0
  10. package/dist/bench/context-attribution.js +0 -0
  11. package/dist/bench/discovery-cost-eval.js +2 -0
  12. package/dist/bench/driver.js +0 -0
  13. package/dist/bench/evals.js +0 -0
  14. package/dist/bench/fake-http-server.js +65 -0
  15. package/dist/bench/instrument.js +0 -0
  16. package/dist/bench/intent-face-eval.js +40 -40
  17. package/dist/bench/run.js +0 -0
  18. package/dist/bench/scenario.js +0 -0
  19. package/dist/bench/types.js +0 -0
  20. package/dist/bi-wire.js +0 -0
  21. package/dist/bizservice-config.js +0 -0
  22. package/dist/call.js +0 -0
  23. package/dist/capability-absences.js +0 -0
  24. package/dist/capability-overview.js +0 -0
  25. package/dist/capability-shape.js +0 -0
  26. package/dist/cli-contract.js +38 -1
  27. package/dist/cli-output.js +8 -2
  28. package/dist/cli.js +147 -5
  29. package/dist/closest-match.js +0 -0
  30. package/dist/code-rules.js +0 -0
  31. package/dist/command-transport.js +0 -0
  32. package/dist/compound-writes.js +0 -0
  33. package/dist/compute.js +0 -0
  34. package/dist/config-store.js +0 -0
  35. package/dist/context.js +0 -0
  36. package/dist/contract-consistency.js +0 -0
  37. package/dist/contract-resources.js +0 -0
  38. package/dist/coverage-consistency.js +0 -0
  39. package/dist/coverage-registry.js +0 -0
  40. package/dist/coverage.js +0 -0
  41. package/dist/data-list-filter.js +0 -0
  42. package/dist/describe-cache.js +0 -0
  43. package/dist/discovery.js +0 -0
  44. package/dist/endpoints.js +0 -0
  45. package/dist/environment-policy.js +0 -0
  46. package/dist/execution-metadata.js +0 -0
  47. package/dist/fake/app.js +45 -0
  48. package/dist/fake/b2-registration.js +594 -0
  49. package/dist/fake/businessrule.js +242 -0
  50. package/dist/fake/datarule.js +72 -0
  51. package/dist/fake/dictionary.js +108 -0
  52. package/dist/fake/environment.js +30 -0
  53. package/dist/fake/field.js +167 -0
  54. package/dist/fake/form.js +101 -0
  55. package/dist/fake/index.js +121 -0
  56. package/dist/fake/list-view.js +289 -0
  57. package/dist/fake/model.js +136 -0
  58. package/dist/fake/online-js.js +18 -0
  59. package/dist/fake/report.js +253 -0
  60. package/dist/fake/routes.js +47 -0
  61. package/dist/fake/rule-lifecycle.js +37 -0
  62. package/dist/fake/runtime-data.js +367 -0
  63. package/dist/fake/state.js +67 -0
  64. package/dist/fake/workflow.js +364 -0
  65. package/dist/field-change.js +86 -2
  66. package/dist/field-families.js +59 -9
  67. package/dist/form-default.js +0 -0
  68. package/dist/form-layout.js +0 -0
  69. package/dist/form-support.js +0 -0
  70. package/dist/formula-lint.js +0 -0
  71. package/dist/goal-routes.js +16 -16
  72. package/dist/governed-execution.js +0 -0
  73. package/dist/graph.js +734 -0
  74. package/dist/human-summary.js +0 -0
  75. package/dist/identity.js +0 -0
  76. package/dist/intent/baseline.js +0 -0
  77. package/dist/intent/capabilities/bizservice.js +0 -0
  78. package/dist/intent/capabilities/businessrule.js +0 -0
  79. package/dist/intent/capabilities/datarule.js +0 -0
  80. package/dist/intent/capabilities/field.js +0 -0
  81. package/dist/intent/capabilities/form.js +0 -0
  82. package/dist/intent/capabilities/index.js +0 -0
  83. package/dist/intent/capabilities/listview.js +0 -0
  84. package/dist/intent/capabilities/model.js +0 -0
  85. package/dist/intent/capabilities/onlinejs.js +0 -0
  86. package/dist/intent/capabilities/report.js +0 -0
  87. package/dist/intent/capabilities/workflow.js +0 -0
  88. package/dist/intent/capability.js +0 -0
  89. package/dist/intent/cli.js +0 -0
  90. package/dist/intent/compare.js +0 -0
  91. package/dist/intent/compiler.js +0 -0
  92. package/dist/intent/dsl.js +0 -0
  93. package/dist/intent/plan-apply.js +0 -0
  94. package/dist/intent/plan-file.js +0 -0
  95. package/dist/intent/readback.js +0 -0
  96. package/dist/intent/schema.js +0 -0
  97. package/dist/intent/validation.js +0 -0
  98. package/dist/intent/yaml.js +0 -0
  99. package/dist/journal.js +0 -0
  100. package/dist/json-column.js +0 -0
  101. package/dist/lanes/app-contract.js +0 -0
  102. package/dist/lanes/app-coverage.js +0 -0
  103. package/dist/lanes/app.js +0 -0
  104. package/dist/lanes/apply-changes.js +0 -0
  105. package/dist/lanes/b2-registration-contract.js +0 -0
  106. package/dist/lanes/b2-registration-coverage.js +0 -0
  107. package/dist/lanes/b2-registration.js +0 -0
  108. package/dist/lanes/businessrule-contract.js +0 -0
  109. package/dist/lanes/businessrule-coverage.js +0 -0
  110. package/dist/lanes/businessrule.js +7 -1
  111. package/dist/lanes/contract-support.js +0 -0
  112. package/dist/lanes/coverage-declaration.js +0 -0
  113. package/dist/lanes/custom-page.js +0 -0
  114. package/dist/lanes/datarule-contract.js +0 -0
  115. package/dist/lanes/datarule-coverage.js +0 -0
  116. package/dist/lanes/datarule-protocol.js +0 -0
  117. package/dist/lanes/datarule.js +0 -0
  118. package/dist/lanes/dictionary-contract.js +0 -0
  119. package/dist/lanes/dictionary-coverage.js +0 -0
  120. package/dist/lanes/dictionary.js +0 -0
  121. package/dist/lanes/environment-contract.js +0 -0
  122. package/dist/lanes/environment-coverage.js +0 -0
  123. package/dist/lanes/environment.js +0 -0
  124. package/dist/lanes/field-contract.js +0 -0
  125. package/dist/lanes/field-coverage.js +0 -0
  126. package/dist/lanes/field.js +6 -1
  127. package/dist/lanes/folder-contract.js +0 -0
  128. package/dist/lanes/folder-coverage.js +0 -0
  129. package/dist/lanes/folder.js +0 -0
  130. package/dist/lanes/form-contract.js +0 -0
  131. package/dist/lanes/form-coverage.js +0 -0
  132. package/dist/lanes/form.js +0 -0
  133. package/dist/lanes/lane-ids.js +0 -0
  134. package/dist/lanes/list-view-contract.js +0 -0
  135. package/dist/lanes/list-view-coverage.js +0 -0
  136. package/dist/lanes/list-view-shapes.js +0 -0
  137. package/dist/lanes/list-view.js +0 -0
  138. package/dist/lanes/meta-contract.js +0 -0
  139. package/dist/lanes/meta.js +6 -0
  140. package/dist/lanes/model-contract.js +0 -0
  141. package/dist/lanes/model-coverage.js +0 -0
  142. package/dist/lanes/model.js +4 -2
  143. package/dist/lanes/online-js-contract.js +0 -0
  144. package/dist/lanes/online-js-coverage.js +0 -0
  145. package/dist/lanes/online-js.js +0 -0
  146. package/dist/lanes/org-tree.js +0 -0
  147. package/dist/lanes/organization.js +0 -0
  148. package/dist/lanes/report-contract.js +0 -0
  149. package/dist/lanes/report-coverage.js +0 -0
  150. package/dist/lanes/report.js +0 -0
  151. package/dist/lanes/rule-graph.js +0 -0
  152. package/dist/lanes/rule-lifecycle-contract.js +0 -0
  153. package/dist/lanes/rule-lifecycle-coverage.js +0 -0
  154. package/dist/lanes/rule-lifecycle.js +0 -0
  155. package/dist/lanes/runtime-data-contract.js +0 -0
  156. package/dist/lanes/runtime-data-coverage.js +0 -0
  157. package/dist/lanes/runtime-data.js +0 -0
  158. package/dist/lanes/workflow-contract.js +0 -0
  159. package/dist/lanes/workflow-coverage.js +0 -0
  160. package/dist/lanes/workflow.js +0 -0
  161. package/dist/model-purge.js +0 -0
  162. package/dist/model-scaffold.js +0 -0
  163. package/dist/omc-core-lazy.js +0 -0
  164. package/dist/online-js-layout.js +0 -0
  165. package/dist/online-js-source.js +28 -28
  166. package/dist/package-tool.js +0 -0
  167. package/dist/package.js +0 -0
  168. package/dist/path-projection.js +0 -0
  169. package/dist/plan.js +0 -0
  170. package/dist/read.js +0 -0
  171. package/dist/recipes/b2-registration.js +0 -0
  172. package/dist/recipes/businessrule.js +0 -0
  173. package/dist/recipes/datarule.js +0 -0
  174. package/dist/recipes/dictionary.js +0 -0
  175. package/dist/recipes/field.js +55 -1
  176. package/dist/recipes/form.js +0 -0
  177. package/dist/recipes/list-view.js +0 -0
  178. package/dist/recipes/model.js +0 -0
  179. package/dist/recipes/online-js.js +0 -0
  180. package/dist/recipes/runtime-data.js +0 -0
  181. package/dist/recipes/shared.js +0 -0
  182. package/dist/recipes/workflow.js +0 -0
  183. package/dist/redact.js +0 -0
  184. package/dist/refusal-guidance.js +0 -0
  185. package/dist/rollback-spec.js +0 -0
  186. package/dist/rollback.js +0 -0
  187. package/dist/rule-support.js +0 -0
  188. package/dist/rule-when.js +0 -0
  189. package/dist/runtime-support.js +0 -0
  190. package/dist/script.js +0 -0
  191. package/dist/server.js +92 -0
  192. package/dist/service-doctor.js +0 -0
  193. package/dist/session-manager.js +0 -0
  194. package/dist/session.js +12 -0
  195. package/dist/skills.js +12 -2
  196. package/dist/sqlite-warning-filter.js +0 -0
  197. package/dist/support.js +0 -0
  198. package/dist/token-cache.js +0 -0
  199. package/dist/tool-types.js +0 -0
  200. package/dist/tools.js +0 -0
  201. package/dist/usage-log.js +0 -0
  202. package/dist/wire.js +0 -0
  203. package/dist/workflow-support.js +0 -0
  204. package/dist/write-lease.js +0 -0
  205. package/dist/write-lock.js +0 -0
  206. package/dist/write.js +0 -0
  207. package/dist/zip.js +0 -0
  208. package/docs/images/omc-nixi/README.md +41 -41
  209. package/docs/omc-introduction.md +851 -851
  210. package/docs/tool-surface-map.md +42 -42
  211. package/package.json +7 -5
  212. package/skills/omc-acceptance-criteria.md +50 -50
  213. package/skills/omc-business-configuration.md +334 -334
  214. package/skills/omc-capabilities.md +214 -214
  215. package/skills/omc-capability-scouting.md +55 -55
  216. package/skills/omc-config-draft-review.md +175 -175
  217. package/skills/omc-five-piece-flow.md +35 -35
  218. package/skills/omc-glossary.md +86 -86
  219. package/skills/omc-refusals.md +127 -127
  220. package/skills/omc-requirement-analysis.md +161 -161
  221. package/skills/omc-requirement-vocabulary.md +61 -61
  222. package/skills/omc-start-here.md +106 -106
  223. package/skills/omc-tool-selection.md +119 -119
  224. package/skills/omc-write-hazards.md +200 -199
  225. package/dist/lanes/organization-coverage.js +0 -27
File without changes
@@ -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 CHANGED
File without changes
package/dist/artifacts.js CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -128,6 +128,8 @@ async function runScenario(scenario) {
128
128
  };
129
129
  }
130
130
  export async function runDiscoveryCostBaseline() {
131
+ // r29(2026-10-29):describe 重复提示读项目 ledger,组合测试顺序会改变它——测量面禁用保证字节确定性。
132
+ process.env.OMC_DESC_REPEAT_HINT = "0";
131
133
  const scenarios = [];
132
134
  for (const scenario of SCENARIOS)
133
135
  scenarios.push(await runScenario(scenario));
File without changes
File without changes
@@ -0,0 +1,65 @@
1
+ import { createServer } from "node:http";
2
+ import { FakeCloudPivot } from "../fake/index.js";
3
+ export async function startFakeHttpServer() {
4
+ const server = createServer();
5
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
6
+ const address = server.address();
7
+ if (address === null || typeof address === "string") {
8
+ server.closeAllConnections?.();
9
+ await new Promise((resolve) => server.close(() => resolve()));
10
+ throw new Error("fake http server has no listen address");
11
+ }
12
+ const url = `http://127.0.0.1:${address.port}`;
13
+ const fake = new FakeCloudPivot({ version: "8.6.26", baseUrl: url });
14
+ server.on("request", (request, response) => {
15
+ const chunks = [];
16
+ request.on("data", (chunk) => chunks.push(chunk));
17
+ request.on("end", () => {
18
+ const target = new URL(request.url ?? "/", url);
19
+ const query = {};
20
+ for (const [key, value] of target.searchParams)
21
+ query[key] = value;
22
+ const raw = Buffer.concat(chunks);
23
+ const contentType = request.headers["content-type"] ?? "";
24
+ const body = raw.length === 0 ? undefined : contentType.includes("json") ? JSON.parse(raw.toString("utf8")) : raw.toString("utf8");
25
+ const headers = {};
26
+ for (const [key, value] of Object.entries(request.headers)) {
27
+ if (typeof value === "string") {
28
+ const capitalized = key.replace(/\b\w/g, (c) => c.toUpperCase()).replace(/-(\w)/g, (_m, c) => `-${c.toUpperCase()}`);
29
+ headers[capitalized] = value;
30
+ }
31
+ }
32
+ fake
33
+ .request({
34
+ path: target.pathname,
35
+ method: request.method === "POST" ? "POST" : "GET",
36
+ ...(Object.keys(query).length > 0 ? { query } : {}),
37
+ ...(body === undefined ? {} : { body }),
38
+ headers,
39
+ })
40
+ .then((result) => {
41
+ const payload = Buffer.isBuffer(result.body) ? result.body : JSON.stringify(result.body);
42
+ response.writeHead(result.status, Buffer.isBuffer(result.body) ? undefined : { "Content-Type": "application/json" });
43
+ response.end(payload);
44
+ })
45
+ .catch(() => {
46
+ response.writeHead(500, { "Content-Type": "application/json" });
47
+ response.end(JSON.stringify({ errcode: 500, errmsg: "fake transport failure", data: null }));
48
+ });
49
+ });
50
+ });
51
+ return {
52
+ fake,
53
+ url,
54
+ close: () => new Promise((resolve, reject) => {
55
+ server.closeAllConnections?.();
56
+ server.close((error) => (error ? reject(error) : resolve()));
57
+ }),
58
+ };
59
+ }
60
+ export function canonicalBusinessCalls(fake) {
61
+ return JSON.stringify(fake.state.callLog
62
+ .filter((entry) => !entry.path.startsWith("/user/") && !entry.path.startsWith("/api/public/") && !entry.path.startsWith("/api/api/organization/"))
63
+ .map((entry) => ({ path: entry.path, method: entry.method, body: entry.body ?? null, query: entry.query && Object.keys(entry.query).length > 0 ? entry.query : null })));
64
+ }
65
+ //# sourceMappingURL=fake-http-server.js.map
File without changes
@@ -122,24 +122,24 @@ function freshHome(url) {
122
122
  writeFileSync(join(home, "credentials"), JSON.stringify({ eval: { username: "tester", password: "eval-face-password" } }));
123
123
  return home;
124
124
  }
125
- const LADDER_INTENT = `intentVersion: 1
126
- models:
127
- - code: eval_m1
128
- name: 评测建模
129
- fields:
130
- - code: f_title
131
- name: 标题
132
- type: SHORT_TEXT
133
- - code: f_amount
134
- name: 金额
135
- type: NUMERICAL
136
- options:
137
- scale: 3
138
- - code: f_when
139
- name: 日期
140
- type: DATE
141
- form:
142
- publish: true
125
+ const LADDER_INTENT = `intentVersion: 1
126
+ models:
127
+ - code: eval_m1
128
+ name: 评测建模
129
+ fields:
130
+ - code: f_title
131
+ name: 标题
132
+ type: SHORT_TEXT
133
+ - code: f_amount
134
+ name: 金额
135
+ type: NUMERICAL
136
+ options:
137
+ scale: 3
138
+ - code: f_when
139
+ name: 日期
140
+ type: DATE
141
+ form:
142
+ publish: true
143
143
  `;
144
144
  export const INTENT_FACE_TASKS = [
145
145
  {
@@ -163,12 +163,12 @@ export const INTENT_FACE_TASKS = [
163
163
  id: "rule-backfill-ladder",
164
164
  description: "two models via intent + DYNAMIC backfill rule + enable + trigger row + verified target readback (mixed face: intent + escape hatch)",
165
165
  async run(agent, server) {
166
- const modelsIntent = agent.writeIntent("models.yaml", `intentVersion: 1
167
- models:
168
- - code: eval_rs
169
- name: 回填源
170
- - code: eval_rt
171
- name: 回填目标
166
+ const modelsIntent = agent.writeIntent("models.yaml", `intentVersion: 1
167
+ models:
168
+ - code: eval_rs
169
+ name: 回填源
170
+ - code: eval_rt
171
+ name: 回填目标
172
172
  `);
173
173
  await agent.intentValidate(modelsIntent);
174
174
  const { plan, planFile } = await agent.intentPlan(modelsIntent);
@@ -209,27 +209,27 @@ models:
209
209
  id: "input-error-recovery",
210
210
  description: "bad intent -> validate exit 10 with source location -> fix -> plan/apply -> idempotent re-plan/re-apply with zero duplicate wires",
211
211
  async run(agent, server) {
212
- const bad = agent.writeIntent("intent.yaml", `intentVersion: 1
213
- models:
214
- - code: eval_idem
215
- name: 幂等模型
216
- fields:
217
- - code: f_bad
218
- name: 坏类型
219
- type: HOLOGRAM
212
+ const bad = agent.writeIntent("intent.yaml", `intentVersion: 1
213
+ models:
214
+ - code: eval_idem
215
+ name: 幂等模型
216
+ fields:
217
+ - code: f_bad
218
+ name: 坏类型
219
+ type: HOLOGRAM
220
220
  `);
221
221
  const rejected = await agent.run(["intent", "validate", bad]);
222
222
  ok(rejected.code === 10 && rejected.envelope.error?.code === "invalid-input", `expected invalid-input exit 10, got ${String(rejected.code)}`);
223
223
  const first = rejected.envelope.result.errors[0];
224
224
  ok(first.code === "intent-field-type-unknown" && typeof first.line === "number", `located error: ${JSON.stringify(first).slice(0, 200)}`);
225
- const fixed = agent.writeIntent("intent.yaml", `intentVersion: 1
226
- models:
227
- - code: eval_idem
228
- name: 幂等模型
229
- fields:
230
- - code: f_bad
231
- name: 改正的字段
232
- type: SHORT_TEXT
225
+ const fixed = agent.writeIntent("intent.yaml", `intentVersion: 1
226
+ models:
227
+ - code: eval_idem
228
+ name: 幂等模型
229
+ fields:
230
+ - code: f_bad
231
+ name: 改正的字段
232
+ type: SHORT_TEXT
233
233
  `);
234
234
  const validated = await agent.intentValidate(fixed);
235
235
  ok(validated.valid === true, `fixed validate: ${JSON.stringify(validated).slice(0, 200)}`);
package/dist/bench/run.js CHANGED
File without changes
File without changes
File without changes
package/dist/bi-wire.js CHANGED
File without changes
File without changes
package/dist/call.js CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -39,15 +39,52 @@ export function asciiStdout() {
39
39
  return true;
40
40
  if (override === "0")
41
41
  return false;
42
+ // Do not spawn chcp on POSIX: it costs a process per invocation and can only
43
+ // ever answer "not a Windows code page here".
44
+ if (process.platform !== "win32")
45
+ return false;
42
46
  return asciiStdoutFor(process.platform, process.stdout.isTTY === true, activeCodePage());
43
47
  }
48
+ /** Escape every non-ASCII code unit to `\uXXXX` (idempotent, JSON-safe). */
49
+ export function escapeNonAscii(text) {
50
+ return text.replace(/[\u007f-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
51
+ }
52
+ /**
53
+ * Apply the SAME code-page policy to stderr diagnostics.
54
+ *
55
+ * stdout was made ASCII-safe years ago, but every human/diagnostic line goes
56
+ * through `console.error`/`process.stderr.write` as raw UTF-8. Under a legacy
57
+ * Windows pipe (`omc … 2>&1 | tee log`, `npm test`, an agent capturing output)
58
+ * that is cp936 vs UTF-8 — the user sees `����` instead of the message. Escaping
59
+ * the same way as the envelope keeps diagnostics decodable, and a real console
60
+ * (TTY) is untouched because Node writes UTF-16 through WriteConsoleW there.
61
+ *
62
+ * Gated on `asciiStdout()` so `OMC_ASCII_STDOUT=0` (what the test suite sets)
63
+ * still yields raw UTF-8 for assertions that match Chinese text.
64
+ */
65
+ let stderrHardened = false;
66
+ export function installCodePageSafeStderr() {
67
+ if (stderrHardened)
68
+ return;
69
+ stderrHardened = true;
70
+ if (!asciiStdout())
71
+ return;
72
+ const original = process.stderr.write.bind(process.stderr);
73
+ process.stderr.write = ((chunk, encodingOrCb, maybeCb) => {
74
+ if (typeof chunk !== "string")
75
+ return original(chunk, encodingOrCb, maybeCb);
76
+ return original(escapeNonAscii(chunk), encodingOrCb, maybeCb);
77
+ });
78
+ }
44
79
  let cachedCodePage;
45
80
  function activeCodePage() {
46
81
  if (cachedCodePage !== undefined)
47
82
  return cachedCodePage;
48
83
  cachedCodePage = null;
49
84
  try {
50
- const chcp = spawnSync("chcp.com", [], { encoding: "utf8" });
85
+ // windowsHide: a GUI host (editor task, shortcut) must not flash a console
86
+ // window for a probe; timeout: never hang a one-shot CLI on chcp.
87
+ const chcp = spawnSync("chcp.com", [], { encoding: "utf8", windowsHide: true, timeout: 2000 });
51
88
  cachedCodePage = /([0-9]{3,5})\s*$/.exec((chcp.stdout || "").trim().split("\n").pop() ?? "")?.[1] ?? null;
52
89
  }
53
90
  catch {
@@ -82,8 +82,14 @@ function emitAsciiBannerOnce() {
82
82
  return;
83
83
  asciiBannerEmitted = true;
84
84
  // r73 win32 实测:管道 + 非 UTF-8 代码页时输出被 ASCII 转义(\uXXXX),用户视为乱码。
85
- // 横幅走 stderr(不进管道、不破坏单信封契约),在痛点时刻给出两个解法。
86
- process.stderr.write("[omc] 当前控制台代码页非 UTF-8,中文已 ASCII 转义防乱码。要原样中文:本次调用加 --utf8,或 chcp 65001 后重跑,或设 OMC_ASCII_STDOUT=0。\n");
85
+ // 横幅走 stderr(不进管道、不破坏单信封契约),在痛点时刻给出解法。
86
+ // 只有「自动判定」成 ascii 时才提示:调用方显式设了 OMC_ASCII_STDOUT/--utf8
87
+ // 说明已知情,再提示就是每次调用多两行噪音。
88
+ // 注意:ascii 模式下 stderr 本身也会被转义,所以横幅必须是纯 ASCII。
89
+ if (process.env.OMC_ASCII_STDOUT !== undefined)
90
+ return;
91
+ process.stderr.write("[omc] non-UTF-8 console code page: non-ASCII is \\u-escaped to survive the pipe. "
92
+ + "Raw text: add --utf8, or `chcp 65001`, or set OMC_ASCII_STDOUT=0.\n");
87
93
  }
88
94
  /**
89
95
  * Attach a repeat warning to a refusal that repeats the previous one. Refusal
package/dist/cli.js CHANGED
@@ -15,10 +15,10 @@ import { BATCH_MAX_ENTRIES, runBatchCall } from "./batch-call.js";
15
15
  import { cmdModelScaffold } from "./model-scaffold.js";
16
16
  import { cmdModelPurge } from "./model-purge.js";
17
17
  import { environmentWritable, requiresProductionConfirmation } from "./environment-policy.js";
18
- import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, refusalText, requestId } from "./cli-contract.js";
18
+ import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, installCodePageSafeStderr, refusalText, requestId } from "./cli-contract.js";
19
19
  import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "./cli-output.js";
20
- import { beginUsage, completeUsage, ensureProjectIgnore, recordUsageOutput, usageReport } from "./usage-log.js";
21
- import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
20
+ import { beginUsage, completeUsage, currentUsageArgsDigest, ensureProjectIgnore, priorIdenticalSuccess, recordUsageOutput, usageReport } from "./usage-log.js";
21
+ import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitThrown, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
22
22
  import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
23
23
  import { projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
24
24
  import { basename, join, resolve } from "node:path";
@@ -44,8 +44,11 @@ import { planIntent, readbackIntent, applyIntent, intentWarnings } from "./inten
44
44
  import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills } from "./skills.js";
45
45
  import { parseToolInput, ToolInputError } from "./tool-types.js";
46
46
  import { WireError, wireCall } from "./wire.js";
47
+ import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
47
48
  import { ENDPOINTS } from "./endpoints.js";
49
+ import { makeToolContext } from "./context.js";
48
50
  import { cmdRollbackList, cmdRollbackPlan, cmdRollbackApply } from "./rollback.js";
51
+ import { SessionManager } from "./session-manager.js";
49
52
  async function ask(question) {
50
53
  const rl = createInterface({ input: stdin, output: process.stderr });
51
54
  try {
@@ -171,6 +174,11 @@ async function cmdDoctor(store, environment, serviceCode) {
171
174
  const session = new CloudPivotSession(resolved.name, resolved.profile, credential);
172
175
  await session.login();
173
176
  steps.push(`登录 ok 企业=${session.enterpriseId()} 管理员=${session.administrator()}`);
177
+ // A deployment whose system/config `client` blob is absent/unreadable still
178
+ // logs in (client_secret is optional on 8.x). Say so as a WARN: it used to
179
+ // abort the login and report env-unreachable on a perfectly usable box.
180
+ for (const note of session.loginNotes())
181
+ steps.push(`OAuth 配置 WARN ${note}`);
174
182
  const version = session.detectedVersion();
175
183
  if (!version) {
176
184
  steps.push(`版本 WARN 无版本证据;写入将保守拒绝(fail closed)`);
@@ -879,8 +887,20 @@ function cmdDiscovery(kind, transport, value, options = {}) {
879
887
  // second identical read returns an artifact reference instead of the body.
880
888
  result = full ? (repeatFullDescribe(value ?? "", full) ?? full) : full;
881
889
  }
882
- else
890
+ else {
883
891
  result = describeTool(registry, value ?? "");
892
+ // r28b 账本(2026-10-28):describe rule.save×11 / form.publish×9——firstCall 教学后
893
+ // 仍反复全量 describe。同参数近期已 ok 时附降载提示,引导 --param/--example 精准取。
894
+ // 无项目 ledger(golden/repo 测试/一次性运行)时 priorIdenticalSuccess 返回
895
+ // undefined,输出保持 byte-stable。
896
+ if (result) {
897
+ const digest = currentUsageArgsDigest();
898
+ const prior = digest === undefined ? undefined : priorIdenticalSuccess(`describe ${String(value ?? "")}`, digest);
899
+ if (prior !== undefined && prior.count >= 1)
900
+ result.describeRepeatHint =
901
+ `同参数 describe 近期已执行过 ${prior.count + 1} 次(含本次)。在找单个参数:omc describe ${String(value ?? "")} --param <参数名>(几百字节);找可抄的完整例子:--example <配方id>;都不必重读整面`;
902
+ }
903
+ }
884
904
  if (!result) {
885
905
  // Name the closest candidates so a near-miss tool name does not cost a
886
906
  // guess-and-retry call (round-4 dogfood: `form.create` / `data.create` /
@@ -1029,6 +1049,106 @@ async function cmdIntentReadback(file, environment, transport) {
1029
1049
  return read.code;
1030
1050
  return emitResult(await readbackIntent(read.intent, environment), transport);
1031
1051
  }
1052
+ /**
1053
+ * ADR-0008 — `omc graph` family: the local derived rule-graph index. A CLI
1054
+ * management face like `intent`/`rollback`: NOT a call tool, not in the tool
1055
+ * registry or `commands --json`. Results are candidate pointers only — plan /
1056
+ * apply / readback keep reading the live baseline.
1057
+ */
1058
+ async function cmdGraphPull(environment, concurrency, transport) {
1059
+ const workers = concurrency ?? DEFAULT_PULL_CONCURRENCY;
1060
+ if (!Number.isSafeInteger(workers) || workers < 1 || workers > MAX_PULL_CONCURRENCY)
1061
+ return emitInvalid(transport, { message: `--concurrency 必须是 1–${MAX_PULL_CONCURRENCY} 的整数(平台负载有界;默认 ${DEFAULT_PULL_CONCURRENCY})` });
1062
+ const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
1063
+ try {
1064
+ const session = await context.sessions.session(environment);
1065
+ const data = await publishGraphPull(session, workers);
1066
+ const edgesByKind = {};
1067
+ for (const edge of data.edges)
1068
+ edgesByKind[edge.kind] = (edgesByKind[edge.kind] ?? 0) + 1;
1069
+ const gapsByKind = {};
1070
+ for (const gap of data.gaps)
1071
+ gapsByKind[gap.kind] = (gapsByKind[gap.kind] ?? 0) + 1;
1072
+ return emitResult({
1073
+ status: "ok",
1074
+ environment: data.meta.environmentName,
1075
+ applicationCode: data.meta.applicationCode,
1076
+ identityFingerprint: data.meta.identityFingerprint,
1077
+ pulledAtIso: data.meta.pulledAtIso,
1078
+ scope: data.meta.scope,
1079
+ freshness: data.gaps.length === 0 ? "complete" : "partial",
1080
+ scanned: { models: data.scanned.models, rules: data.scanned.rules, datarules: data.scanned.datarules, views: data.scanned.views, ...(data.scanned.parseFailures > 0 ? { parseFailures: data.scanned.parseFailures } : {}) },
1081
+ edges: edgesByKind,
1082
+ ...(data.gaps.length > 0 ? { gaps: { count: data.gaps.length, byKind: gapsByKind } } : {}),
1083
+ dbBytes: data.dbBytes,
1084
+ timings: data.timings,
1085
+ nextCommand: "omc graph references <kind> <code>",
1086
+ }, transport);
1087
+ }
1088
+ catch (error) {
1089
+ if (error instanceof GraphPullLockHeldError)
1090
+ return emitResult({ status: "refused", blocker: "graph-pull-lock-held", message: error.message, nextCommand: "omc graph status" }, transport);
1091
+ if (error instanceof WireError)
1092
+ return emitResult({ status: "uncertain", reason: "wire-transport", message: `graph pull 中断(${error.message});索引未发布,重跑 omc graph pull --env <env>` }, transport);
1093
+ return emitThrown(error, transport);
1094
+ }
1095
+ }
1096
+ function cmdGraphReferences(kind, code, transport) {
1097
+ if (!["service", "model", "field"].includes(kind))
1098
+ return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
1099
+ try {
1100
+ const result = queryGraphReferences(kind, code);
1101
+ return emitResult({
1102
+ status: "ok",
1103
+ kind: result.kind,
1104
+ code: result.code,
1105
+ matches: result.matches,
1106
+ index: result.index,
1107
+ ...(result.note !== undefined ? { note: result.note } : {}),
1108
+ ...(result.gapWarning !== undefined ? { gapWarning: result.gapWarning } : {}),
1109
+ }, transport);
1110
+ }
1111
+ catch (error) {
1112
+ if (error instanceof GraphIndexAbsentError)
1113
+ return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message, nextCommand: "omc graph pull --env <env>" }, transport);
1114
+ return emitThrown(error, transport);
1115
+ }
1116
+ }
1117
+ function cmdGraphStatus(transport) {
1118
+ try {
1119
+ const status = readGraphStatus();
1120
+ return emitResult({ status: "ok", index: status.index, dbBytes: status.dbBytes, gaps: status.gaps }, transport);
1121
+ }
1122
+ catch (error) {
1123
+ if (error instanceof GraphIndexAbsentError)
1124
+ return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message, nextCommand: "omc graph pull --env <env>" }, transport);
1125
+ return emitThrown(error, transport);
1126
+ }
1127
+ }
1128
+ function cmdGraphImpact(kind, code, transport) {
1129
+ if (!["service", "model", "field"].includes(kind))
1130
+ return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
1131
+ try {
1132
+ const result = impactGraph(kind, code);
1133
+ return emitResult({ status: "ok", ...result }, transport);
1134
+ }
1135
+ catch (error) {
1136
+ if (error instanceof GraphIndexAbsentError)
1137
+ return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message, nextCommand: "omc graph pull --env <env>" }, transport);
1138
+ return emitThrown(error, transport);
1139
+ }
1140
+ }
1141
+ function cmdGraphSearch(term, transport) {
1142
+ try {
1143
+ const result = searchGraphRules(term);
1144
+ return emitResult({ status: "ok", ...result }, transport);
1145
+ }
1146
+ catch (error) {
1147
+ if (error instanceof GraphIndexAbsentError)
1148
+ return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message, nextCommand: "omc graph pull --env <env>" }, transport);
1149
+ return emitThrown(error, transport);
1150
+ }
1151
+ }
1032
1152
  /** Ticket 119 — `omc intent apply <plan-file>`: plan integrity + baseline drift gates, then serial execution through the EXISTING apply_changes machinery. */
1033
1153
  async function cmdIntentApply(planPath, options, transport) {
1034
1154
  return applyIntent(planPath, options, {
@@ -1341,6 +1461,9 @@ function joinSplitFlagValues(raw, flags) {
1341
1461
  }
1342
1462
  async function main() {
1343
1463
  let commandExitCode = 0;
1464
+ // stderr is the diagnostics stream: apply the same Windows code-page policy
1465
+ // as stdout before ANY message is written (banner, doctor, refusals).
1466
+ installCodePageSafeStderr();
1344
1467
  beginUsage(argv);
1345
1468
  const command = new Command();
1346
1469
  const ownPackageVersion = () => {
@@ -1714,6 +1837,25 @@ async function main() {
1714
1837
  intentCommand.command("readback <file>").option("--env <environment>").option("--max-output-bytes <bytes>").action(async (file, options) => {
1715
1838
  commandExitCode = await withTransport(options, request, (transport) => cmdIntentReadback(file, options.env, transport));
1716
1839
  });
1840
+ // ADR-0008: local derived rule-graph index — CLI management face, not a call
1841
+ // tool (so it stays out of `commands --json`/coverage by construction).
1842
+ const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008):pull 全量重建(有界并发)/ references 离线引用 / impact 影响面 / search 规则全文 / status 完整性与新鲜度。候选定位器——不进 baseline/plan/写门禁");
1843
+ graphCommand.command("pull").option("--env <environment>").option("--environment <environment>").option("--concurrency <n>", "并发 wire 读数(1–32,默认 16):拉取已流水线化,墙钟 ≈ max(模型面,规则面)/并发 × 延迟;过高可能触发平台限流形成缺口").option("--max-output-bytes <bytes>").addHelpText("after", "\n只读远端(model.list → 每模型 rule.list+datarule.list → 每规则 rule.get,统一任务池流水线并发)+ 本地原子重建;不写环境、不占环境写锁。新鲜度随结果回报(complete|partial),查询走 graph references / impact / search。\n").action(async (options) => {
1844
+ const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
1845
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
1846
+ });
1847
+ graphCommand.command("references <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;结果带索引新鲜度与扫描缺口(有缺口时零命中不构成「无引用」证明)。\n").action(async (kind, code, options) => {
1848
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport));
1849
+ });
1850
+ graphCommand.command("impact <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n影响面(离线):跳1=引用该编码的规则(同 references),跳2=这些规则还读写哪些 service/model/field——改前看爆炸半径。截断上限 50 规则×每类 20 码。\n").action(async (kind, code, options) => {
1851
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
1852
+ });
1853
+ graphCommand.command("search <term...>").option("--max-output-bytes <bytes>").addHelpText("after", "\n规则全文检索(code+name,FTS5,末词前缀匹配):找到\"关于 X 的规则\"拿 code,再走 references/impact。中文需给全词或词首(如 报名*)。\n").action(async (term, options) => {
1854
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphSearch(term.join(" "), transport));
1855
+ });
1856
+ graphCommand.command("status").option("--max-output-bytes <bytes>").action(async (options) => {
1857
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphStatus(transport));
1858
+ });
1717
1859
  // `--jq` expressions the shell split arrive as several tokens; re-join them.
1718
1860
  await command.parseAsync(joinSplitFlagValues(stripOutputFlags(argv), ["--jq"]));
1719
1861
  return commandExitCode;
@@ -1742,7 +1884,7 @@ main().then((code) => {
1742
1884
  // envelope (or, for `env --json`, their own JSON) and were printing a second
1743
1885
  // empty envelope after it (live 2026-10-23: `omc rollback apply` answered two
1744
1886
  // lines, violating the one-envelope stdout contract).
1745
- const ownsStdout = ["call", "script", "artifact", "commands", "describe", "contract", "capabilities", "capability", "next", "usage", "intent", "help", "form-default", "compute", "rule-when", "approval-flow", "model-scaffold", "model-purge", "rollback", "env", "--version", "-V"];
1887
+ const ownsStdout = ["call", "script", "artifact", "commands", "describe", "contract", "capabilities", "capability", "next", "usage", "intent", "help", "form-default", "compute", "rule-when", "approval-flow", "model-scaffold", "model-purge", "rollback", "env", "graph", "--version", "-V"];
1746
1888
  if (ownsStdout.includes(argv[2] ?? "")) {
1747
1889
  completeUsage(code);
1748
1890
  finish(code);
File without changes
File without changes
File without changes
File without changes
package/dist/compute.js CHANGED
File without changes
File without changes
package/dist/context.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/coverage.js CHANGED
File without changes
File without changes
File without changes