@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,99 @@
1
+ import { ENDPOINTS } from "./endpoints.js";
2
+ import { wireCall } from "./wire.js";
3
+ /**
4
+ * Shared workflow RUNTIME endpoint knowledge (ticket 21): merges the two
5
+ * parallel protocol implementations that used to live inside the runtime-data
6
+ * and workflow lanes, mirroring form-support / rule-support. Design-time
7
+ * workflow authoring (template read/write/publish) stays in the workflow
8
+ * lane — this module owns only:
9
+ *
10
+ * - GET /runtime/workflow/search_workitems — the caller's pending todos
11
+ * (server injects userId); WorkItemSearchVO.filterWorkflowInstanceId is a
12
+ * NOT-equal exclusion (BizWorkitemServiceImpl.toSearchWorkItemsCriterion:
13
+ * 400-402), so exact instance matching is client-side and that server
14
+ * field is never sent.
15
+ * - GET /runtime/workflow/getBizObjectByWfInstanceId — in-flight bizObject
16
+ * identity (BizObjectCreatedModel.id/schemaCode).
17
+ * - GET /runtime/form/getReplayToken — replay token the isStartWf branches
18
+ * verify server-side (AbstractFormRuntimeController.verifyReplay, else
19
+ * 100005).
20
+ *
21
+ * Deliberately NOT here (grilled seam, ticket 21): status polling (form/load
22
+ * readback semantics — runtime-data), formType branching, submit payload
23
+ * assembly, and the two-phase draft orchestration.
24
+ */
25
+ /** Tolerant string decode for wire fields (empty/non-string → null). */
26
+ export function wireString(value) {
27
+ return typeof value === "string" && value ? value : null;
28
+ }
29
+ /**
30
+ * Decode the `PageVO<WorkItemVO>` payload of search_workitems:
31
+ * `{ totalPages, totalElements, content }` serialized directly; a `page`/`data`
32
+ * wrapper is tolerated for resilience. The single decoder for both lanes
33
+ * (ticket 21: the two per-lane extract* twins merged into one).
34
+ */
35
+ export function decodeWorkItemPage(value) {
36
+ const root = value && typeof value === "object" ? value : {};
37
+ const candidate = root.page && typeof root.page === "object" ? root.page : root;
38
+ const content = Array.isArray(candidate.content)
39
+ ? candidate.content.filter((row) => !!row && typeof row === "object")
40
+ : [];
41
+ return {
42
+ total: typeof candidate.totalElements === "number" ? candidate.totalElements : 0,
43
+ totalPages: typeof candidate.totalPages === "number" ? candidate.totalPages : 0,
44
+ content,
45
+ };
46
+ }
47
+ /**
48
+ * Discover the caller's pending work items on the evidenced endpoint and
49
+ * apply the client-side exact instance filter when requested. Returns the
50
+ * page-level totals plus the (filtered) content rows.
51
+ */
52
+ export async function searchWorkItems(session, filter = {}) {
53
+ const query = {
54
+ page: String(filter.page ?? 0),
55
+ size: String(filter.size ?? 200),
56
+ };
57
+ if (filter.workflowCode)
58
+ query.workflowCode = filter.workflowCode;
59
+ if (filter.schemaCode)
60
+ query.schemaCode = filter.schemaCode;
61
+ const page = decodeWorkItemPage((await wireCall(session, { path: ENDPOINTS.workflowSearchWorkItems, query })).data);
62
+ const items = filter.instanceId
63
+ ? page.content.filter((item) => wireString(item.instanceId) === filter.instanceId)
64
+ : page.content;
65
+ return { total: page.total, totalPages: page.totalPages, items };
66
+ }
67
+ /**
68
+ * Resolve the in-flight bizObject identity for a workflow instance.
69
+ * Authoritative source: getBizObjectByWfInstanceId → BizObjectCreatedModel
70
+ * .id/.schemaCode. Best-effort by design: a failed lookup leaves the
71
+ * identity null and never throws — a resolution failure must not drop the
72
+ * todo it belongs to.
73
+ */
74
+ export async function resolveBizObjectIdByInstance(session, instanceId) {
75
+ try {
76
+ const object = await wireCall(session, {
77
+ path: ENDPOINTS.workflowGetBizObjectByWfInstanceId,
78
+ query: { workflowInstanceId: instanceId },
79
+ });
80
+ const data = (object.data ?? {});
81
+ return {
82
+ bizObjectId: wireString(data.id) ?? wireString(data.objectId),
83
+ schemaCode: wireString(data.schemaCode),
84
+ };
85
+ }
86
+ catch {
87
+ return { bizObjectId: null, schemaCode: null };
88
+ }
89
+ }
90
+ /**
91
+ * Fetch a replay token from GET /runtime/form/getReplayToken. Returns
92
+ * undefined when the face yields no usable string — callers then omit the
93
+ * key entirely (a token-less request must not carry an empty value).
94
+ */
95
+ export async function getReplayToken(session) {
96
+ const token = await wireCall(session, { path: ENDPOINTS.runtimeFormReplayToken, query: {} });
97
+ return typeof token.data === "string" && token.data ? token.data : undefined;
98
+ }
99
+ //# sourceMappingURL=workflow-support.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-environment write lease: concurrent write operations must serialize.
3
+ * A write acquires the lease; a second concurrent write waits (bounded) for
4
+ * the first to complete rather than interleaving. Enforced across the whole
5
+ * CLI process, keyed by environment name.
6
+ */
7
+ export class WriteLeaseQueue {
8
+ chains = new Map();
9
+ active = new Map();
10
+ run(environment, operationId, operation) {
11
+ const previous = this.chains.get(environment) ?? Promise.resolve();
12
+ const result = previous.then(operation, operation);
13
+ this.chains.set(environment, result.then(() => undefined, () => undefined));
14
+ this.active.set(environment, operationId);
15
+ const clear = () => {
16
+ if (this.active.get(environment) === operationId)
17
+ this.active.delete(environment);
18
+ };
19
+ result.then(clear, clear);
20
+ return result;
21
+ }
22
+ holder(environment) {
23
+ return this.active.get(environment);
24
+ }
25
+ }
26
+ //# sourceMappingURL=write-lease.js.map
@@ -0,0 +1,109 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ function locksRoot() {
5
+ return join(resolve(process.env.OMC_CONFIG_HOME ?? join(process.cwd(), ".omc")), "locks");
6
+ }
7
+ function lockTarget(environment) {
8
+ const encoded = environment.replace(/[^a-zA-Z0-9_-]/g, (char) => [...char].map((c) => `-${c.charCodeAt(0).toString(16)}`).join(""));
9
+ return join(locksRoot(), encoded);
10
+ }
11
+ function ownerFile(environment) {
12
+ return `${lockTarget(environment)}.lock.owner.json`;
13
+ }
14
+ function readOwner(environment) {
15
+ try {
16
+ return JSON.parse(readFileSync(ownerFile(environment), "utf8"));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function writeOwner(environment) {
23
+ const owner = { pid: process.pid, startedAt: new Date().toISOString(), command: process.argv.slice(0, 4).join(" ") };
24
+ try {
25
+ writeFileSync(ownerFile(environment), JSON.stringify(owner, null, 2) + "\n", { mode: 0o600 });
26
+ }
27
+ catch {
28
+ // metadata only: the filesystem lock itself remains authoritative
29
+ }
30
+ }
31
+ function clearOwner(environment) {
32
+ try {
33
+ rmSync(ownerFile(environment), { force: true });
34
+ }
35
+ catch {
36
+ // metadata only
37
+ }
38
+ }
39
+ function pidAlive(pid) {
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ }
44
+ catch (error) {
45
+ return error.code === "EPERM";
46
+ }
47
+ }
48
+ function breakStaleLock(environment) {
49
+ rmSync(`${lockTarget(environment)}.lock`, { recursive: true, force: true });
50
+ clearOwner(environment);
51
+ }
52
+ async function tryAcquire(environment) {
53
+ mkdirSync(locksRoot(), { recursive: true });
54
+ try {
55
+ const release = await lockfile.lock(lockTarget(environment), { retries: 0, realpath: false, stale: 30_000 });
56
+ writeOwner(environment);
57
+ return {
58
+ held: true,
59
+ release: async () => {
60
+ clearOwner(environment);
61
+ try {
62
+ await release();
63
+ }
64
+ catch {
65
+ // already released or broken by a stale recovery
66
+ }
67
+ },
68
+ };
69
+ }
70
+ catch (error) {
71
+ const code = error.code;
72
+ if (code !== "ELOCKED")
73
+ throw error;
74
+ const owner = readOwner(environment);
75
+ if (owner && Number.isInteger(owner.pid) && !pidAlive(owner.pid)) {
76
+ breakStaleLock(environment);
77
+ return tryAcquire(environment);
78
+ }
79
+ const lockDir = `${lockTarget(environment)}.lock`;
80
+ const ownerDead = owner !== null && Number.isInteger(owner.pid) && !pidAlive(owner.pid);
81
+ return {
82
+ held: false,
83
+ reason: "conflict",
84
+ owner,
85
+ recovery: [
86
+ `another process holds the write lock for environment ${environment}`,
87
+ owner ? `owner pid=${owner.pid} started=${owner.startedAt} command=${owner.command}` : "owner metadata missing (the holder predates lock metadata or crashed while acquiring)",
88
+ `lock path: ${lockDir}`,
89
+ ...(ownerDead
90
+ ? ["owner pid is NOT alive → this is a STALE lock; re-running the same command now recovers automatically (or delete the lock path above)"]
91
+ : ["the holder is alive → wait for it to finish, then retry (retryable:true); do NOT delete the lock path while the holder runs"]),
92
+ `to force recovery when the owner is dead: rm -rf "${lockDir}" then retry`,
93
+ ],
94
+ };
95
+ }
96
+ }
97
+ export async function acquireWriteLock(environment) {
98
+ return tryAcquire(environment);
99
+ }
100
+ export function lockOwner(environment) {
101
+ return readOwner(environment);
102
+ }
103
+ export function staleLockPresent(environment) {
104
+ if (!existsSync(`${lockTarget(environment)}.lock`))
105
+ return false;
106
+ const owner = readOwner(environment);
107
+ return owner !== null && Number.isInteger(owner.pid) && !pidAlive(owner.pid);
108
+ }
109
+ //# sourceMappingURL=write-lock.js.map
package/dist/write.js ADDED
@@ -0,0 +1,176 @@
1
+ import { z } from "zod";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ /** Parse a JSON string for the dry-lint; unparseable input returns undefined so the shape validators report it. */
6
+ function parseJsonOrUndefined(text) {
7
+ try {
8
+ return JSON.parse(text);
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ import { admitGovernedWrite } from "./governed-execution.js";
15
+ import { writeSummary } from "./human-summary.js";
16
+ import { planFor } from "./plan.js";
17
+ import { isUncertainWireError, sha256, wireErrorKind, writeArtifact } from "./support.js";
18
+ import { capabilityArg, confirmProductionArg, environmentArg, WRITE_PROTOCOL } from "./args.js";
19
+ import { plannableArgsRaw } from "./plan.js";
20
+ import { defineTool, ToolRefusalError } from "./tool-types.js";
21
+ import { assertFieldFamilyAllowed } from "./field-families.js";
22
+ import { validateBusinessCode } from "./code-rules.js";
23
+ import { validateFormLayout } from "./form-layout.js";
24
+ import { prepareRuleGraph, validateRuleNodeGraph } from "./lanes/rule-graph.js";
25
+ import { WireError } from "./wire.js";
26
+ /**
27
+ * The shared governed write pipeline and the low-level write registration
28
+ * factory every lane adapter uses.
29
+ */
30
+ /**
31
+ * Shared governed write pipeline: coverage gate → environment write permission
32
+ * → session → version-family gate (fail closed without evidence) → production
33
+ * confirmation → write lease → execute with authoritative readback.
34
+ */
35
+ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
36
+ const requestFingerprint = args.requestId ? sha256({ requestId: args.requestId, args: plannableArgsRaw(args) }) : undefined;
37
+ // The shared admission owns the gate sequence; this face only supplies its
38
+ // plan and its dry-run / replay payload shapes.
39
+ const outcome = await admitGovernedWrite(ctx, {
40
+ environment: args.environment,
41
+ confirmProduction: args.confirmProduction,
42
+ dryRun: args.dryRun,
43
+ capability: args.capability,
44
+ coverage: { lane: args.capability ?? tool.lane, scope: tool.scope ?? "" },
45
+ plan: (session) => planFor(ctx, args.environment, tool.name, args),
46
+ // A face may supply a session-aware dry-run projection (e.g. model.delete's
47
+ // reference scan) so the preview carries facts that only live reads can
48
+ // establish — never a second write path.
49
+ dryRunResult: dryRunPayload ? (plan, session) => dryRunPayload(session, args).then((payload) => ({ status: "dry-run", wouldChange: true, plan, idempotency: idempotencyDeclaration(tool.name), ...payload, ...(args.requestId ? { requestId: args.requestId } : {}) })) : (plan) => ({ status: "dry-run", wouldChange: true, plan, idempotency: idempotencyDeclaration(tool.name), ...(args.requestId ? { requestId: args.requestId } : {}) }),
50
+ replay: (plan) => (requestFingerprint ? findCompleted(ctx.artifactsDir, args.requestId, tool.name, plan.environment, requestFingerprint) : undefined),
51
+ });
52
+ if (outcome.kind === "settled")
53
+ return outcome.result;
54
+ const { session, plan, productionConfirmed } = outcome.admission;
55
+ return ctx.lease.run(session.environmentName, `${tool.name}:${randomUUID().slice(0, 8)}`, async () => {
56
+ let executed;
57
+ try {
58
+ executed = await execute(session);
59
+ }
60
+ catch (error) {
61
+ if (isUncertainWireError(error))
62
+ return {
63
+ status: "uncertain",
64
+ frozen: true,
65
+ reason: `wire-transport:${wireErrorKind(error)}`,
66
+ detail: error.message,
67
+ message: "write outcome cannot be established; frozen — re-discover before retrying; never assume success",
68
+ };
69
+ if (error instanceof ToolRefusalError)
70
+ throw error;
71
+ if (error instanceof WireError)
72
+ return { status: "refused", blocker: `wire-${error.kind}`, errcode: error.errcode, message: error.message, ...(error.diagnostic ? { diagnostic: error.diagnostic } : {}) };
73
+ throw error;
74
+ }
75
+ const { performed, readback } = executed;
76
+ // Every production write echoes the explicit acknowledgement into the audit
77
+ // trail, so the record can always answer "was this a confirmed production
78
+ // write?".
79
+ const artifact = {
80
+ tool: tool.name,
81
+ ...(args.requestId ? { requestId: args.requestId, requestFingerprint } : {}),
82
+ environment: session.environmentName,
83
+ environmentFingerprint: session.environmentFingerprint(),
84
+ performed,
85
+ readback,
86
+ ...(productionConfirmed ? { productionConfirmed: true } : {}),
87
+ deliveryState: "DELIVERED",
88
+ recordedAt: new Date().toISOString(),
89
+ };
90
+ const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
91
+ return { status: "ok", deliveryState: "DELIVERED", performed, readback, ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
92
+ });
93
+ }
94
+ function idempotencyDeclaration(tool) {
95
+ if (tool.includes("delete"))
96
+ return { key: "requestId", replay: "completed", duplicate: "already-absent-no-op" };
97
+ if (tool.includes("publish"))
98
+ return { key: "requestId", replay: "completed", duplicate: "already-published-no-op" };
99
+ if (tool.includes("import"))
100
+ return { key: "requestId", replay: "completed", duplicate: "same-request-replay-no-op" };
101
+ return { key: "requestId", replay: "completed", duplicate: "same-request-replay-no-op" };
102
+ }
103
+ function findCompleted(root, requestId, tool, environment, requestFingerprint) {
104
+ if (!existsSync(root))
105
+ return undefined;
106
+ for (const name of readdirSync(root)) {
107
+ if (!name.endsWith(".json"))
108
+ continue;
109
+ try {
110
+ const artifact = JSON.parse(readFileSync(join(root, name), "utf8"));
111
+ if (artifact.deliveryState === "DELIVERED" && artifact.requestId === requestId && artifact.tool === tool && artifact.environment === environment && artifact.requestFingerprint === requestFingerprint)
112
+ return { status: "ok", deliveryState: "DELIVERED", replay: "already-completed", requestId, idempotency: idempotencyDeclaration(tool), performed: artifact.performed, readback: artifact.readback };
113
+ }
114
+ catch { }
115
+ }
116
+ return undefined;
117
+ }
118
+ /** Low-level write registration factory: shared by every lane adapter. */
119
+ export function writeTool(tools, name, lane, scope, description, inputSchema, execute, preflight, dryRunPayload) {
120
+ const rawExecute = execute;
121
+ const semanticGate = (args) => {
122
+ if (name === "field.create")
123
+ assertFieldFamilyAllowed(args);
124
+ // CFG-naming: reject an invalid business code BEFORE the write permission
125
+ // gate and any wire call (length / style / reserved system codes).
126
+ if (name === "model.create" || name === "field.create") {
127
+ const violations = validateBusinessCode(String(args.code ?? ""), { kind: name === "model.create" ? "model" : "field" });
128
+ if (violations.length > 0)
129
+ throw new ToolRefusalError({ status: "refused", blocker: "business-code-invalid", message: "code 未过出门前校验;按 validation 修正(长度/风格/保留字)", validation: violations });
130
+ }
131
+ // CFG-form-layout: a supplied draft layout must satisfy the platform's
132
+ // span/row/coverage invariants before it reaches the environment.
133
+ if (name === "form.draft" && typeof args.draftViewJson === "string" && typeof args.draftAttributesJson === "string") {
134
+ const violations = validateFormLayout(parseJsonOrUndefined(args.draftViewJson), parseJsonOrUndefined(args.draftAttributesJson));
135
+ if (violations.length > 0)
136
+ throw new ToolRefusalError({ status: "refused", blocker: "form-layout-invalid", message: "表单布局未过出门前校验;按 validation 修正(span 白名单/行宽≤24/整行独占/控件覆盖率)", validation: violations });
137
+ }
138
+ if (name === "rule.save") {
139
+ // Pre-flight dry lint (ticket 14): refuse a mis-shaped node graph
140
+ // BEFORE the write permission gate and before any wire call. CFG-02/CFG-26: lint
141
+ // the SAME normalized graph the executor will send (prepareRuleGraph) —
142
+ // linting the raw args would refuse a graph the executor normalizes and
143
+ // would wave through the shapes it does not touch.
144
+ const ruleCode = String(args.ruleCode ?? "");
145
+ const prepared = prepareRuleGraph(ruleCode, (args.nodeGraph ?? {}), args.customTrigger, typeof args.schemaCode === "string" ? args.schemaCode : undefined);
146
+ const blockers = [...prepared.warnings, ...validateRuleNodeGraph(prepared.graph, ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined)];
147
+ // Ticket 113: warning-severity findings ride the payload but never gate.
148
+ if (blockers.some((blocker) => blocker.severity !== "warning"))
149
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-graph-validation-failed", message: "nodeGraph 未过出门前校验(dry lint);按 validation 逐项修正,校验器不猜不改", validation: blockers });
150
+ }
151
+ // Optional per-tool semantic gate: refuse before the write permission gate
152
+ // and before any wire call, independent of input-schema validation.
153
+ if (preflight)
154
+ preflight(args);
155
+ };
156
+ tools.push({
157
+ ...defineTool({
158
+ name,
159
+ layer: "low",
160
+ kind: "write",
161
+ lane,
162
+ scope,
163
+ description: `${description} ${WRITE_PROTOCOL}`,
164
+ inputSchema: { environment: environmentArg, capability: capabilityArg, confirmProduction: confirmProductionArg, requestId: z.string().min(1).optional(), dryRun: z.boolean().optional(), ...inputSchema },
165
+ handler: async (ctx, args, self) => {
166
+ semanticGate(args);
167
+ return governedWrite(ctx, self, args, (session) => rawExecute(session, args), dryRunPayload);
168
+ },
169
+ }),
170
+ // Ticket 46: the batch entry drives the SAME executor/gates, so a batch
171
+ // step can never bypass its lane's readback or semantic refusal.
172
+ writeExecute: rawExecute,
173
+ writePreflight: semanticGate,
174
+ });
175
+ }
176
+ //# sourceMappingURL=write.js.map
package/dist/zip.js ADDED
@@ -0,0 +1,156 @@
1
+ import { inflateRawSync } from "node:zlib";
2
+ /**
3
+ * Ticket 106 — a minimal, dependency-free ZIP reader/writer for the bizservice
4
+ * IE package face.
5
+ *
6
+ * The platform export answers a ZIP with manifest.json + version.md
7
+ * (BizServiceIEController.java:124-138). Java's ZipOutputStream typically
8
+ * writes local headers with zeroed sizes plus a trailing data descriptor
9
+ * (general-purpose bit 3), so the READER resolves entries through the central
10
+ * directory (whose sizes/CRCs are authoritative) and only uses each local
11
+ * header to locate the data offset. The WRITER emits stored (method 0)
12
+ * entries — real zips for fake routes and tests; the platform reader accepts
13
+ * both stored and deflated entries.
14
+ */
15
+ const EOCD_SIGNATURE = 0x06054b50;
16
+ const CENTRAL_SIGNATURE = 0x02014b50;
17
+ const LOCAL_SIGNATURE = 0x04034b50;
18
+ const EOCD_SCAN_LIMIT = 65_557; // fixed EOCD (22) + max comment (65_535)
19
+ export class ZipFormatError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "ZipFormatError";
23
+ }
24
+ }
25
+ /** CRC-32 (IEEE 802.3), table-driven. */
26
+ const CRC_TABLE = (() => {
27
+ const table = new Array(256);
28
+ for (let n = 0; n < 256; n += 1) {
29
+ let c = n;
30
+ for (let k = 0; k < 8; k += 1)
31
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
32
+ table[n] = c >>> 0;
33
+ }
34
+ return table;
35
+ })();
36
+ function crc32(buffer) {
37
+ let crc = 0xffffffff;
38
+ for (const byte of buffer)
39
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
40
+ return (crc ^ 0xffffffff) >>> 0;
41
+ }
42
+ /** Locate the End Of Central Directory record; returns its byte offset. */
43
+ function locateEocd(buffer) {
44
+ const start = Math.max(0, buffer.length - EOCD_SCAN_LIMIT);
45
+ for (let offset = buffer.length - 22; offset >= start; offset -= 1) {
46
+ if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
47
+ const commentLength = buffer.readUInt16LE(offset + 20);
48
+ if (offset + 22 + commentLength === buffer.length)
49
+ return offset;
50
+ }
51
+ }
52
+ throw new ZipFormatError("no end-of-central-directory record found");
53
+ }
54
+ /** Read every central-directory entry as name → decompressed Buffer. */
55
+ export function readZipEntries(buffer) {
56
+ const eocd = locateEocd(buffer);
57
+ const entryCount = buffer.readUInt16LE(eocd + 10);
58
+ let cursor = buffer.readUInt32LE(eocd + 16);
59
+ const entries = new Map();
60
+ for (let index = 0; index < entryCount; index += 1) {
61
+ if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE)
62
+ throw new ZipFormatError(`central directory entry ${index} is malformed`);
63
+ const method = buffer.readUInt16LE(cursor + 10);
64
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
65
+ const nameLength = buffer.readUInt16LE(cursor + 28);
66
+ const extraLength = buffer.readUInt16LE(cursor + 30);
67
+ const commentLength = buffer.readUInt16LE(cursor + 32);
68
+ const localOffset = buffer.readUInt32LE(cursor + 42);
69
+ const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
70
+ // The local header carries its OWN name/extra lengths (they may differ
71
+ // from the central copies); the data starts right after them.
72
+ if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE)
73
+ throw new ZipFormatError(`local header for "${name}" is malformed`);
74
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
75
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
76
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
77
+ const data = buffer.subarray(dataStart, dataStart + compressedSize);
78
+ if (method === 0)
79
+ entries.set(name, Buffer.from(data));
80
+ else if (method === 8)
81
+ entries.set(name, inflateRawSync(data));
82
+ else
83
+ throw new ZipFormatError(`entry "${name}" uses unsupported compression method ${method}`);
84
+ cursor += 46 + nameLength + extraLength + commentLength;
85
+ }
86
+ return entries;
87
+ }
88
+ /** Build a stored-entry ZIP (method 0, UTF-8 names flagged via bit 11). */
89
+ export function buildZip(files) {
90
+ const chunks = [];
91
+ const central = [];
92
+ let offset = 0;
93
+ for (const file of files) {
94
+ const nameBytes = Buffer.from(file.name, "utf8");
95
+ const crc = crc32(file.data);
96
+ const local = Buffer.alloc(30);
97
+ local.writeUInt32LE(LOCAL_SIGNATURE, 0);
98
+ local.writeUInt16LE(0x0800, 6); // UTF-8 name flag
99
+ local.writeUInt16LE(0, 8); // stored
100
+ local.writeUInt16LE(0, 10); // time
101
+ local.writeUInt16LE(0x2100, 12); // date (1996-01-01, deterministic)
102
+ local.writeUInt32LE(crc, 14);
103
+ local.writeUInt32LE(file.data.length, 18);
104
+ local.writeUInt32LE(file.data.length, 22);
105
+ local.writeUInt16LE(nameBytes.length, 26);
106
+ local.writeUInt16LE(0, 28);
107
+ chunks.push(local, nameBytes, file.data);
108
+ const entry = Buffer.alloc(46);
109
+ entry.writeUInt32LE(CENTRAL_SIGNATURE, 0);
110
+ entry.writeUInt16LE(0x031e, 4); // made by UNIX/ZIP 3.0
111
+ entry.writeUInt16LE(0x0014, 6); // extract by DOS/ZIP 2.0
112
+ entry.writeUInt16LE(0x0800, 8);
113
+ entry.writeUInt16LE(0, 10);
114
+ entry.writeUInt16LE(0, 12);
115
+ entry.writeUInt16LE(0x2100, 14);
116
+ entry.writeUInt32LE(crc, 16);
117
+ entry.writeUInt32LE(file.data.length, 20);
118
+ entry.writeUInt32LE(file.data.length, 24);
119
+ entry.writeUInt16LE(nameBytes.length, 28);
120
+ entry.writeUInt16LE(0, 30);
121
+ entry.writeUInt16LE(0, 32);
122
+ entry.writeUInt16LE(0, 34);
123
+ entry.writeUInt16LE(0, 36);
124
+ entry.writeUInt32LE(0, 38);
125
+ entry.writeUInt32LE(offset, 42);
126
+ central.push(entry, nameBytes);
127
+ offset += 30 + nameBytes.length + file.data.length;
128
+ }
129
+ const centralBuffer = Buffer.concat(central);
130
+ const eocd = Buffer.alloc(22);
131
+ eocd.writeUInt32LE(EOCD_SIGNATURE, 0);
132
+ eocd.writeUInt16LE(0, 4);
133
+ eocd.writeUInt16LE(0, 6);
134
+ eocd.writeUInt16LE(files.length, 8);
135
+ eocd.writeUInt16LE(files.length, 10);
136
+ eocd.writeUInt32LE(centralBuffer.length, 12);
137
+ eocd.writeUInt32LE(offset, 16);
138
+ eocd.writeUInt16LE(0, 20);
139
+ return Buffer.concat([...chunks, centralBuffer, eocd]);
140
+ }
141
+ /**
142
+ * Resolve an entry by BASENAME: the platform export compresses the staging
143
+ * DIRECTORY, so entries carry a `bizService-<uuid>/` prefix
144
+ * (live 2026-09-15: bizService-59d72d4a…/manifest.json).
145
+ */
146
+ export function zipEntryByBasename(entries, basename) {
147
+ const direct = entries.get(basename);
148
+ if (direct !== undefined)
149
+ return direct;
150
+ const suffix = `/${basename}`;
151
+ for (const [name, data] of entries)
152
+ if (name.endsWith(suffix))
153
+ return data;
154
+ return undefined;
155
+ }
156
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1,42 @@
1
+ # Tool Surface ↔ Coverage Manifest Correspondence
2
+
3
+ Baseline: `coverage/m1-coverage-manifest-v1.json` (39 items, M1, 8.6-only).
4
+ Every `executable` and `partial` item maps to at least one tool below;
5
+ `blocked` / `static-only` / `unsupported` items are hard boundaries —
6
+ requesting them refuses with the manifest citation (red-team tested).
7
+
8
+ Methodology (how to operate the tools) is read with `omc help <topic>` — see
9
+ `skills/`. The tool surface is the "what"; those topics are the "how".
10
+
11
+ | # | Manifest item (lane) | Status | Tools that carry it |
12
+ |---|---|---|---|
13
+ | 1 | W1-list-view | partial | `listview.create`, `listview.delete` (forced defaults, header readback, exact absence) |
14
+ | 3 | W5-S26-aggregation | partial | `field.create` with formula/EXPRESSION scope (same-model AGGREGATION semantics documented in the manifest entry; no separate wire face was live-proven) |
15
+ | 10 | W7-S29-greenfield | executable | `workflow.create`, `workflow.delete` |
16
+ | 11 | W7-S29-residual-OQs | partial | surfaced by `coverage.manifest` + `get_status` (residual OQs are documented unknowns — no tool may claim past them) |
17
+ | 12 | S25-rule-lifecycle | executable | `rule.enable`, `rule.disable`, `rule.delete` (invoke via enable + effect readback) |
18
+ | 12a | B1-businessrule | executable | `rule.list` (latest runtime versions with full business-field projection; agent filters client-side) |
19
+ | 13 | S32-business-integration | executable | `service.register`, `service.get`, `service.delete` (RESTFUL face) |
20
+ | 15 | M2-runtime-data (data-entry projection) | partial | `data.load` (projection boundary: `form/load` authoritative) |
21
+ | 15a | M2-runtime-data (query/list) | executable | `data.list` (simplified filters -> queryCondition, pagination, field projection) |
22
+ | 21 | field-families | executable | `field.create`, `field.publish`, `field.delete`, `field.list` |
23
+ | 22 | field-families (numeric-logical/date-time) | partial | same field tools, family types constrained to the proven set |
24
+ | 23 | model-management | executable | `model.create`, `model.update` (app-tree NODE rename patch: bizSchemaCode/id/name/parentId required, modelType refused — fixed at create), `model.setSummary`, `model.delete`, `model.deleteMany` (publish aspect = model-scoped publication via `field.publish`/`form.publish`, exactly the proven ladder; no separate model-publish wire call exists) |
25
+ | 24 | form-publish-chain | executable | `form.draft`, `form.publish`, `form.get` (draft-first, explicit publishedHtmlJson, control-level layout readback) |
26
+ | 25 | B1-businessrule | executable | `rule.save` (dead-state + B1.2 CRUD/conditional node graphs) |
27
+ | 26 | C-workflow-template | executable | `workflow.create` (minimal template lifecycle) |
28
+ | 27 | online-js (composer) | executable | `onlinejs.compose` (platform skeleton preserved, canonical customScript element with array attrs, publish + SHA-256 readback). The design-time 在线开发 face (`/api/api/programming` JAVA source) is authored via 二开扩展包 and exposes no CLI tool |
29
+ | 28 | M2-runtime-data (write/readback) | executable | `data.save`, `data.load`, `data.delete`, `data.list` |
30
+ | 29 | B2-registration | executable | `service.register` (+ method), `service.get`, `service.delete` |
31
+ | 30 | environment-governance | executable | `discover_environment`, `environment.fingerprint`, `doctor`, `get_status` |
32
+ | 31 | e2e-delivery | executable | the governed chain itself: write tools (DELIVERED + artifact) → read tools (VERIFIED) → delete tools with exact-absence proof (CLEANED) |
33
+ | 32 | app-management | executable | `app.list`, `app.create`, `app.delete` (design-time application package lifecycle; production writes permanently refused) |
34
+
35
+ High-level orchestrations compose the same lanes under one token:
36
+ `configure_model` (23+21+24), `configure_form` (24), `configure_rule`
37
+ (25+12), `discover_environment` (30).
38
+
39
+ Blocked/static-only/unsupported boundaries (no tools, enforced by refusal):
40
+ W2-permissions, W5-S26-cross-model, W6-S27-* (nested child tables, relation
41
+ linkage, child-to-child), W6-S28-*, S32-custom-extension, boundary-unsupported
42
+ (inline backend coding, app publish/environment release, OCR/ELECTRONIC_SIGN).