@stalfh233/omc-cli 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. package/skills/omc-write-hazards.md +87 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Ticket 117 — minimal YAML subset parser for the declarative intent file.
3
+ *
4
+ * Scope (deliberately small, zero dependencies): block mappings, block
5
+ * sequences, plain/quoted scalars (string / number / boolean / null), comments,
6
+ * and single-line flow collections (`[a, b]` / `{k: v}` — desugared to the same
7
+ * located tree). Every node carries its source {line, column} (1-based) so the
8
+ * semantic layer can point validation errors at the exact offending key.
9
+ *
10
+ * Out of scope by refusal: multi-line flow collections, anchors/aliases,
11
+ * multi-document streams, block scalars (| and >), tags. Hitting any of these
12
+ * fails closed with a located yaml-syntax-error.
13
+ */
14
+ export class YamlSyntaxError extends Error {
15
+ line;
16
+ column;
17
+ constructor(line, column, message) {
18
+ super(message);
19
+ this.line = line;
20
+ this.column = column;
21
+ }
22
+ }
23
+ /** Strip comments (a `#` preceded by start-of-line or whitespace, outside quotes) from a line body. */
24
+ function stripComment(body) {
25
+ let inSingle = false;
26
+ let inDouble = false;
27
+ for (let index = 0; index < body.length; index += 1) {
28
+ const char = body[index];
29
+ if (char === "'" && !inDouble)
30
+ inSingle = !inSingle;
31
+ else if (char === '"' && !inSingle)
32
+ inDouble = !inDouble;
33
+ else if (char === "#" && !inSingle && !inDouble && (index === 0 || /\s/.test(body[index - 1])))
34
+ return body.slice(0, index).trimEnd();
35
+ }
36
+ return body;
37
+ }
38
+ /** Split the source into non-empty logical lines with their indentation. */
39
+ function logicalLines(text) {
40
+ const lines = [];
41
+ const rawLines = text.split(/\r?\n/);
42
+ for (let index = 0; index < rawLines.length; index += 1) {
43
+ const raw = rawLines[index];
44
+ const stripped = stripComment(raw);
45
+ if (stripped.trim().length === 0)
46
+ continue;
47
+ if (stripped.trim() === "---" || stripped.trim() === "...")
48
+ throw new YamlSyntaxError(index + 1, 1, "multi-document streams are not part of the intent YAML subset");
49
+ const indent = stripped.length - stripped.trimStart().length;
50
+ lines.push({ text: stripped.trim(), line: index + 1, indent });
51
+ }
52
+ return lines;
53
+ }
54
+ /** Parse a scalar token; quoted strings keep their content verbatim, plain tokens map to typed values. */
55
+ function parseScalar(token, line, column) {
56
+ if (token.startsWith("[") || token.startsWith("{"))
57
+ return parseFlow(token, line, column);
58
+ if (token === "[]")
59
+ return { kind: "seq", items: [], line, column };
60
+ if (token === "{}")
61
+ return { kind: "map", entries: [], line, column };
62
+ if (token.startsWith('"')) {
63
+ if (!token.endsWith('"') || token.length < 2)
64
+ throw new YamlSyntaxError(line, column, "unterminated double-quoted string");
65
+ const inner = token.slice(1, -1);
66
+ if (inner.includes('"'))
67
+ throw new YamlSyntaxError(line, column, "unescaped quote inside double-quoted string");
68
+ return { kind: "scalar", value: inner, line, column };
69
+ }
70
+ if (token.startsWith("'")) {
71
+ if (!token.endsWith("'") || token.length < 2)
72
+ throw new YamlSyntaxError(line, column, "unterminated single-quoted string");
73
+ const inner = token.slice(1, -1);
74
+ if (inner.includes("'"))
75
+ throw new YamlSyntaxError(line, column, "unescaped quote inside single-quoted string");
76
+ return { kind: "scalar", value: inner, line, column };
77
+ }
78
+ if (token.startsWith("|") || token.startsWith(">"))
79
+ throw new YamlSyntaxError(line, column, "block scalars (| and >) are not part of the intent YAML subset");
80
+ if (token.includes(": "))
81
+ throw new YamlSyntaxError(line, column, `unexpected ": " inside plain scalar "${token}"`);
82
+ if (token === "true")
83
+ return { kind: "scalar", value: true, line, column };
84
+ if (token === "false")
85
+ return { kind: "scalar", value: false, line, column };
86
+ if (token === "null" || token === "~")
87
+ return { kind: "scalar", value: null, line, column };
88
+ if (/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(token))
89
+ return { kind: "scalar", value: Number(token), line, column };
90
+ if (token === "-" || token === "?")
91
+ throw new YamlSyntaxError(line, column, "bare dash/question mark is not a valid scalar in the intent subset");
92
+ if (token.startsWith("&") || token.startsWith("*"))
93
+ throw new YamlSyntaxError(line, column, "anchors/aliases are not part of the intent YAML subset");
94
+ return { kind: "scalar", value: token, line, column };
95
+ }
96
+ /** Split a flow body on top-level `delimiter`, respecting nested [] / {} and quotes. */
97
+ function splitFlow(body, delimiter, line, column) {
98
+ const parts = [];
99
+ let depth = 0;
100
+ let inSingle = false;
101
+ let inDouble = false;
102
+ let start = 0;
103
+ for (let index = 0; index < body.length; index += 1) {
104
+ const char = body[index];
105
+ if (char === "'" && !inDouble)
106
+ inSingle = !inSingle;
107
+ else if (char === '"' && !inSingle)
108
+ inDouble = !inDouble;
109
+ else if (!inSingle && !inDouble) {
110
+ if (char === "[" || char === "{")
111
+ depth += 1;
112
+ else if (char === "]" || char === "}")
113
+ depth -= 1;
114
+ else if (char === delimiter && depth === 0) {
115
+ parts.push(body.slice(start, index));
116
+ start = index + 1;
117
+ }
118
+ }
119
+ }
120
+ if (inSingle || inDouble)
121
+ throw new YamlSyntaxError(line, column, "unterminated quoted string in flow collection");
122
+ if (depth !== 0)
123
+ throw new YamlSyntaxError(line, column, "unbalanced [] / {} inside a flow collection");
124
+ parts.push(body.slice(start));
125
+ return parts;
126
+ }
127
+ /** Strip a matching pair of surrounding quotes from a flow key. */
128
+ function flowKey(raw) {
129
+ const token = raw.trim();
130
+ if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'")))
131
+ return token.slice(1, -1);
132
+ return token;
133
+ }
134
+ /**
135
+ * Parse a single-line flow collection. A multi-line flow collection (the closing
136
+ * bracket is not on the same line) is refused with a located error — the intent
137
+ * subset is line-oriented, so a block-style rewrite is the intended fix.
138
+ */
139
+ function parseFlow(token, line, column) {
140
+ const open = token[0];
141
+ const close = open === "[" ? "]" : "}";
142
+ if (token[token.length - 1] !== close || token.length < 2)
143
+ throw new YamlSyntaxError(line, column, `flow collection must open and close on one line ("${token}"): rewrite as a block sequence/mapping`);
144
+ const inner = token.slice(1, -1).trim();
145
+ if (open === "[") {
146
+ if (inner === "")
147
+ return { kind: "seq", items: [], line, column };
148
+ const items = splitFlow(inner, ",", line, column).map((part) => {
149
+ const item = part.trim();
150
+ if (item === "")
151
+ throw new YamlSyntaxError(line, column, "empty item in flow sequence");
152
+ return parseScalar(item, line, column);
153
+ });
154
+ return { kind: "seq", items, line, column };
155
+ }
156
+ if (inner === "")
157
+ return { kind: "map", entries: [], line, column };
158
+ const entries = splitFlow(inner, ",", line, column).map((part) => {
159
+ const item = part.trim();
160
+ if (item === "")
161
+ throw new YamlSyntaxError(line, column, "empty entry in flow mapping");
162
+ const pair = splitFlow(item, ":", line, column);
163
+ if (pair.length < 2)
164
+ throw new YamlSyntaxError(line, column, `flow mapping entry "${item}" is missing a "key: value"`);
165
+ const key = flowKey(pair[0]);
166
+ if (key.length === 0)
167
+ throw new YamlSyntaxError(line, column, "empty key in flow mapping");
168
+ const rest = pair.slice(1).join(":").trim();
169
+ return { key, keyLine: line, keyColumn: column, value: rest === "" ? { kind: "scalar", value: null, line, column } : parseScalar(rest, line, column) };
170
+ });
171
+ return { kind: "map", entries, line, column };
172
+ }
173
+ const isMapLine = (text) => !/^[[{]/.test(text.trim()) && (/^"[^"]+"\s*:/.test(text) || /^'[^']+'\s*:/.test(text) || /^[^:\s][^:]*:(\s|$)/.test(text));
174
+ /** Split "key: rest" — key may be quoted; rest may be empty (nested block follows). */
175
+ function splitKey(text, line, column) {
176
+ const quoted = /^"([^"]+)"\s*:(.*)$/.exec(text) ?? /^'([^']+)'\s*:(.*)$/.exec(text);
177
+ if (quoted)
178
+ return { key: quoted[1], rest: quoted[2].trim() };
179
+ const match = /^([^:]+):\s*(.*)$/.exec(text);
180
+ if (!match)
181
+ throw new YamlSyntaxError(line, column, `expected "key: value" mapping, got "${text}"`);
182
+ return { key: match[1].trim(), rest: match[2].trim() };
183
+ }
184
+ /**
185
+ * Recursive block parser. Mutates `lines` in ONE place: a sequence dash
186
+ * carrying an inline mapping start ("- code: x") is rewritten in place to
187
+ * the stripped mapping line at dash-column + 2, then parsed as the nested
188
+ * block — the standard desugaring keeps position math trivial.
189
+ */
190
+ function parseBlock(lines, cursor, indent) {
191
+ const first = lines[cursor.index];
192
+ if (first.text === "-" || first.text.startsWith("- ")) {
193
+ const items = [];
194
+ while (cursor.index < lines.length) {
195
+ const current = lines[cursor.index];
196
+ if (current.indent < indent)
197
+ break;
198
+ if (current.indent > indent)
199
+ throw new YamlSyntaxError(current.line, current.indent + 1, "inconsistent sequence item indentation");
200
+ if (current.text !== "-" && !current.text.startsWith("- "))
201
+ break;
202
+ const dashColumn = current.indent + 1;
203
+ const rest = current.text === "-" ? "" : current.text.slice(2).trim();
204
+ cursor.index += 1;
205
+ if (rest.length === 0) {
206
+ if (cursor.index < lines.length && lines[cursor.index].indent > indent)
207
+ items.push(parseBlock(lines, cursor, lines[cursor.index].indent));
208
+ else
209
+ items.push({ kind: "scalar", value: null, line: current.line, column: dashColumn });
210
+ continue;
211
+ }
212
+ if (isMapLine(rest)) {
213
+ // Desugar "- key: value" -> nested mapping starting after the dash.
214
+ lines[cursor.index - 1] = { text: rest, line: current.line, indent: indent + 2 };
215
+ cursor.index -= 1;
216
+ items.push(parseBlock(lines, cursor, indent + 2));
217
+ continue;
218
+ }
219
+ items.push(parseScalar(rest, current.line, indent + 3));
220
+ }
221
+ return { kind: "seq", items, line: first.line, column: indent + 1 };
222
+ }
223
+ const entries = [];
224
+ while (cursor.index < lines.length) {
225
+ const current = lines[cursor.index];
226
+ if (current.indent < indent)
227
+ break;
228
+ if (current.indent > indent)
229
+ throw new YamlSyntaxError(current.line, current.indent + 1, "inconsistent mapping key indentation");
230
+ if (current.text === "-" || current.text.startsWith("- "))
231
+ throw new YamlSyntaxError(current.line, current.indent + 1, "sequence item inside a mapping (check the dash indentation)");
232
+ const colonOffset = current.text.indexOf(":");
233
+ const { key, rest } = splitKey(current.text, current.line, current.indent + 1);
234
+ const valueColumn = current.indent + 1 + (colonOffset >= 0 ? colonOffset + 2 : current.text.length + 1);
235
+ cursor.index += 1;
236
+ if (rest.length === 0) {
237
+ if (cursor.index < lines.length && lines[cursor.index].indent > indent) {
238
+ entries.push({ key, keyLine: current.line, keyColumn: current.indent + 1, value: parseBlock(lines, cursor, lines[cursor.index].indent) });
239
+ }
240
+ else {
241
+ entries.push({ key, keyLine: current.line, keyColumn: current.indent + 1, value: { kind: "scalar", value: null, line: current.line, column: valueColumn } });
242
+ }
243
+ continue;
244
+ }
245
+ entries.push({ key, keyLine: current.line, keyColumn: current.indent + 1, value: parseScalar(rest, current.line, valueColumn) });
246
+ }
247
+ if (entries.length === 0)
248
+ throw new YamlSyntaxError(first.line, indent + 1, "empty mapping block");
249
+ return { kind: "map", entries, line: first.line, column: indent + 1 };
250
+ }
251
+ /** Parse an intent YAML document into a located node tree. */
252
+ export function parseYaml(text) {
253
+ const lines = logicalLines(text);
254
+ if (lines.length === 0)
255
+ throw new YamlSyntaxError(1, 1, "intent file is empty");
256
+ const rootIndent = lines[0].indent;
257
+ for (const line of lines.slice(1)) {
258
+ if (line.indent < rootIndent)
259
+ throw new YamlSyntaxError(line.line, line.indent + 1, "top-level indentation must be consistent (the root block starts at one column)");
260
+ }
261
+ const cursor = { index: 0 };
262
+ const node = parseBlock(lines, cursor, rootIndent);
263
+ if (cursor.index < lines.length) {
264
+ const leftover = lines[cursor.index];
265
+ throw new YamlSyntaxError(leftover.line, leftover.indent + 1, `unexpected content after the document ("${leftover.text}")`);
266
+ }
267
+ return node;
268
+ }
269
+ /** Plain JS value of a located tree (positions dropped). */
270
+ export function toPlain(node) {
271
+ if (node.kind === "scalar")
272
+ return node.value;
273
+ if (node.kind === "seq")
274
+ return node.items.map(toPlain);
275
+ return Object.fromEntries(node.entries.map((entry) => [entry.key, toPlain(entry.value)]));
276
+ }
277
+ /** Walk a "a.b[0].c"-style path to its node, for error reporting. */
278
+ export function nodeAt(node, path) {
279
+ const segments = path.split(".");
280
+ let current = node;
281
+ for (const rawSegment of segments) {
282
+ if (current === undefined)
283
+ return undefined;
284
+ const arrayMatch = /^([^\[]+)\[(\d+)\]$/.exec(rawSegment);
285
+ if (arrayMatch) {
286
+ if (current.kind !== "map")
287
+ return undefined;
288
+ const entry = current.entries.find((candidate) => candidate.key === arrayMatch[1]);
289
+ if (entry === undefined || entry.value.kind !== "seq")
290
+ return undefined;
291
+ current = entry.value.items[Number(arrayMatch[2])];
292
+ continue;
293
+ }
294
+ if (current.kind !== "map")
295
+ return undefined;
296
+ const entry = current.entries.find((candidate) => candidate.key === rawSegment);
297
+ if (entry === undefined)
298
+ return undefined;
299
+ current = entry.value;
300
+ }
301
+ return current;
302
+ }
303
+ /** Locate a mapping KEY (not its value) — the pointer shown for key-level errors. */
304
+ export function locateKey(node, path) {
305
+ const lastSlash = path.lastIndexOf(".");
306
+ const parent = lastSlash === -1 ? node : nodeAt(node, path.slice(0, lastSlash));
307
+ const key = lastSlash === -1 ? path : path.slice(lastSlash + 1);
308
+ if (parent === undefined || parent.kind !== "map")
309
+ return undefined;
310
+ const entry = parent.entries.find((candidate) => candidate.key === key);
311
+ if (entry === undefined)
312
+ return undefined;
313
+ return { line: entry.keyLine, column: entry.keyColumn };
314
+ }
315
+ //# sourceMappingURL=yaml.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Ticket 52 — one explicit home for "a platform JSON column may arrive as a
3
+ * string (or an object)" decoding/encoding. Previously every lane carried a
4
+ * local copy with subtly different behaviour; the deliberate differences are
5
+ * now NAMED POLICIES chosen at each call site:
6
+ *
7
+ * - `JSON_COLUMN_PASSTHROUGH` — non-strings pass through untouched
8
+ * (undefined stays undefined); unparseable strings return verbatim.
9
+ * (datarule options, field.list readback)
10
+ * - `JSON_COLUMN_NULL_COALESCE` — non-strings null-coalesce (undefined →
11
+ * null); blank/unparseable strings return verbatim ("" stays "").
12
+ * (businessrule node/route/schedulerSetting, b2 configJson/parameter faces,
13
+ * rule-support parameter faces, list-view condition options)
14
+ * - `{ invalid: "throw" }` — a corrupt column is a hard error
15
+ * (field.update merge base reads the stored options strictly).
16
+ *
17
+ * Blank strings ("", " ") are not valid JSON, so they always take the
18
+ * `invalid` branch; at every previous site that meant passthrough.
19
+ */
20
+ export const JSON_COLUMN_PASSTHROUGH = { nonString: "passthrough", invalid: "passthrough" };
21
+ export const JSON_COLUMN_NULL_COALESCE = { nonString: "null", invalid: "passthrough" };
22
+ export function decodeJsonColumn(value, policy) {
23
+ if (typeof value !== "string")
24
+ return policy.nonString === "null" ? value ?? null : value;
25
+ try {
26
+ return JSON.parse(value);
27
+ }
28
+ catch (error) {
29
+ if (policy.invalid === "throw")
30
+ throw error;
31
+ return value;
32
+ }
33
+ }
34
+ /** Wire encode: a string is sent as-is, anything else is JSON-stringified. */
35
+ export function encodeJsonColumn(value) {
36
+ return typeof value === "string" ? value : JSON.stringify(value);
37
+ }
38
+ /**
39
+ * Decode an options face that may be a JSON string or already an object into
40
+ * a plain record (or undefined). Policies preserve the previous per-site
41
+ * semantics: relation options accept strings/objects/arrays loosely; the
42
+ * FORMULA formulaConfig validator only accepts strings that parse to a
43
+ * non-array object; field.update/form-scaffold strict reads throw on corrupt
44
+ * strings.
45
+ */
46
+ export function decodeJsonObjectColumn(value, policy) {
47
+ const asRecord = (parsed) => {
48
+ if (!parsed || typeof parsed !== "object")
49
+ return undefined;
50
+ if (Array.isArray(parsed))
51
+ return policy.array === "reject" ? undefined : parsed;
52
+ return parsed;
53
+ };
54
+ if (typeof value === "string") {
55
+ try {
56
+ return asRecord(JSON.parse(value));
57
+ }
58
+ catch (error) {
59
+ if (policy.invalid === "throw")
60
+ throw error;
61
+ return undefined;
62
+ }
63
+ }
64
+ if (policy.object === "reject")
65
+ return undefined;
66
+ return asRecord(value);
67
+ }
68
+ //# sourceMappingURL=json-column.js.map
@@ -0,0 +1,95 @@
1
+ import { assertDescriptionCarries, assertLaneContract, behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION } from "./contract-support.js";
2
+ import { LANES } from "./lane-ids.js";
3
+ /**
4
+ * app-management wire facts (live-proven). The default-group resolution, the
5
+ * publish boundary, and the whole-record update quirk are the single source
6
+ * for the handler descriptions and the coverage declarations.
7
+ */
8
+ export const APP_WIRE = Object.freeze({
9
+ createBody: "apppackage/create body {groupId, code, name, name_i18n}",
10
+ createReadiness: "app.create refuses missing/unresolved application groups before create wire (zero wire); readback carries selectedGroup, postconditions, publishBoundary, recovery",
11
+ defaultGroupResolution: "explicit groupId > configured app's groupId (auto-configured-app-group) > tenant first named non-fallback group (auto-first-group, warning:resolved-to-first-group); a fallback-group landing (code=all) warns resolved-to-fallback-group; portalHidden:true",
12
+ publishBoundary: "app publish is out of boundary; the operator must publish in the designer and verify the group, otherwise the portal is invisible",
13
+ updateBody: "apppackage/update submits the whole record by id; an explicit groupId takes effect, an omitted groupId is forced back to the default group, and enabled!==true clears workflow favorites — so app.update re-reads the record and only changes groupId while preserving the stored enabled value",
14
+ deleteBody: "apppackage/delete?appCode=; APP_PACKAGE_HAS_NODE refuses when the app still has non-folder nodes (delete models first)",
15
+ deletePoll: "apppackage/list polls to exact absence",
16
+ });
17
+ export const APP_LANE_CONTRACT = Object.freeze({
18
+ lane: LANES.app,
19
+ family: CONTRACT_FAMILY,
20
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
21
+ endpoints: Object.freeze({
22
+ list: "applicationList",
23
+ groupList: "applicationGroupList",
24
+ get: "applicationGet",
25
+ create: "applicationCreate",
26
+ update: "applicationUpdate",
27
+ delete: "applicationDelete",
28
+ }),
29
+ wire: Object.freeze({
30
+ createBody: APP_WIRE.createBody,
31
+ createReadiness: APP_WIRE.createReadiness,
32
+ defaultGroupResolution: APP_WIRE.defaultGroupResolution,
33
+ publishBoundary: APP_WIRE.publishBoundary,
34
+ updateBody: APP_WIRE.updateBody,
35
+ deleteBody: APP_WIRE.deleteBody,
36
+ deletePoll: APP_WIRE.deletePoll,
37
+ }),
38
+ wireBindings: Object.freeze({
39
+ createBody: Object.freeze([behaviorBinding("test/app-management.test.ts#app.create plans, executes under approval, and reads back presence", assertAppContract)]),
40
+ createReadiness: Object.freeze([behaviorBinding("test/ticket-04-environment-readiness.test.ts#app.create refuses missing or unresolved groups before the create wire", assertAppContract)]),
41
+ defaultGroupResolution: Object.freeze([behaviorBinding("test/app-management.test.ts#app.create default group follows the configured application's group", assertAppContract)]),
42
+ publishBoundary: Object.freeze([behaviorBinding("test/app-management.test.ts#app.create falls back to the first group with a warning", assertAppContract)]),
43
+ updateBody: Object.freeze([behaviorBinding("test/app-management.test.ts#app.update preserves false enabled and does not invent i18n fields", assertAppContract)]),
44
+ deleteBody: Object.freeze([behaviorBinding("test/app-management.test.ts#app.delete removes the app via the admin delete endpoint", assertAppContract)]),
45
+ deletePoll: Object.freeze([behaviorBinding("test/app-management.test.ts#app.delete of an absent app is a no-op with proven absence", assertAppContract)]),
46
+ }),
47
+ evidence: Object.freeze([
48
+ "packages/omc-cli/test/app-management.test.ts",
49
+ "packages/omc-cli/src/lanes/app.ts",
50
+ ".scratch/omc-app-management/issues/01-app-create-default-group-unreachable.md",
51
+ ".scratch/omc-app-management/issues/04-app-move-group-capability.md",
52
+ ]),
53
+ tools: Object.freeze({
54
+ "app.list": Object.freeze({
55
+ scope: "app package list",
56
+ description: "列出应用(apppackage/list)及其所属分组;用于跨应用解析,以及 app.create/app.delete 前后的读回。",
57
+ }),
58
+ "app.create": Object.freeze({
59
+ scope: "app package create",
60
+ description: "创建一个设计时应用(apppackage/create)。groupId 可选,缺省解析配置应用所在分组(操作者必然可见);配置应用不存在时回退租户第一个具名非兜底分组(auto-first-group,warning:resolved-to-first-group)。落到兜底分组 code=all 时读回带 warning:resolved-to-fallback-group; portalHidden:true。创建后以 apppackage/list 读回存在性。应用发布属边界外:建成后需人工在设计器发布并核对该分组,否则门户不可见。",
61
+ }),
62
+ "app.update": Object.freeze({
63
+ scope: "app package update",
64
+ description: "更新一个应用(apppackage/update,管理员面;8.6.26 源码证据:AppPackageController.update → AppIntegrationFacadeImpl.updateAppPackage,按记录 id 整体提交,显式 groupId 生效,缺省 groupId 会被强制回默认分组,enabled 非 true 会清 workflow 收藏——故本工具读回记录后仅改 groupId 并原样保留 enabled)。当前仅支持改 `groupId`(应用迁移分组的补救路径),不隐式补写 i18nMessage,name 等其他字段以读回值原样回传。以 apppackage/list 读回分组变更。",
65
+ }),
66
+ "app.delete": Object.freeze({
67
+ scope: "app package delete absence",
68
+ description: "删除一个应用(apppackage/delete?appCode=,管理员面:级联删除其下模型数据)。要求应用下无非目录节点,否则以 APP_PACKAGE_HAS_NODE 拒绝——先删模型再删应用。以 apppackage/list 轮询读回精确缺席。必须显式传 `code`,没有默认应用,避免误删。",
69
+ }),
70
+ }),
71
+ });
72
+ export function assertAppContract() {
73
+ assertLaneContract(APP_LANE_CONTRACT);
74
+ if (!APP_WIRE.defaultGroupResolution.includes("resolved-to-first-group"))
75
+ throw new Error("app contract pins the first-group fallback warning token");
76
+ assertDescriptionCarries(APP_LANE_CONTRACT, "app.list", ["apppackage/list", "跨应用"]);
77
+ assertDescriptionCarries(APP_LANE_CONTRACT, "app.create", ["apppackage/create", "resolved-to-first-group", "边界外"]);
78
+ if (!APP_WIRE.createReadiness.includes("zero"))
79
+ throw new Error("app contract must pin the zero-wire prerequisite refusal");
80
+ assertDescriptionCarries(APP_LANE_CONTRACT, "app.update", ["apppackage/update", "groupId", "enabled", "原样保留", "AppPackageController.update"]);
81
+ assertDescriptionCarries(APP_LANE_CONTRACT, "app.delete", ["apppackage/delete", "APP_PACKAGE_HAS_NODE", "缺席"]);
82
+ if (!APP_WIRE.createBody.includes("apppackage/create"))
83
+ throw new Error("app contract must pin the create endpoint");
84
+ if (!APP_WIRE.defaultGroupResolution.includes("auto-first-group"))
85
+ throw new Error("app contract must pin the first-group fallback");
86
+ if (!APP_WIRE.publishBoundary.includes("out of boundary"))
87
+ throw new Error("app contract must pin the publish boundary");
88
+ if (!APP_WIRE.updateBody.includes("only changes groupId"))
89
+ throw new Error("app contract must pin the whole-record update quirk");
90
+ if (!APP_WIRE.deleteBody.includes("APP_PACKAGE_HAS_NODE"))
91
+ throw new Error("app contract must pin the delete precondition");
92
+ if (!APP_WIRE.deletePoll.includes("exact absence"))
93
+ throw new Error("app contract must pin the delete absence poll");
94
+ }
95
+ //# sourceMappingURL=app-contract.js.map
@@ -0,0 +1,16 @@
1
+ import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
+ import { APP_LANE_CONTRACT } from "./app-contract.js";
3
+ const T = APP_LANE_CONTRACT.tools;
4
+ export const APP_COVERAGE = declareLaneCoverage(APP_LANE_CONTRACT.lane, [
5
+ {
6
+ capability: "Application package (app) lifecycle: list, create, update (group move), admin delete",
7
+ evidence: ["packages/omc-cli/test/app-management.test.ts", "packages/omc-cli/src/lanes/app.ts"],
8
+ tools: [
9
+ claim("app.list", T["app.list"].scope),
10
+ claim("app.create", T["app.create"].scope),
11
+ claim("app.update", T["app.update"].scope),
12
+ claim("app.delete", T["app.delete"].scope),
13
+ ],
14
+ },
15
+ ]);
16
+ //# sourceMappingURL=app-coverage.js.map