@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,79 @@
1
+ import { capabilitiesForModel, orderedCapabilities } from "./capabilities/index.js";
2
+ const absentForm = { present: false, published: false, existDraft: false, boundFieldCodes: [] };
3
+ const absentListview = { present: false, columns: [] };
4
+ /** A model the baseline did not cover still plans as fully absent (defensive). */
5
+ function absentModel(code) {
6
+ return { code, name: null, present: false, fields: [], form: absentForm, listview: absentListview };
7
+ }
8
+ /**
9
+ * Compile an intent against its baseline into ordered changes + skips.
10
+ *
11
+ * Ordering is CAPABILITY-major: for each capability in dependency order, plan
12
+ * every model. This is what makes declared `requires` hold ACROSS models — a
13
+ * rule on model A that references model B's fields needs every model's fields
14
+ * published before any rule is planned. (Single-model intents are unaffected;
15
+ * multi-model intents order by capability instead of by model, which is the
16
+ * correct dependency order.) The emitted arrays are local, so a refusal thrown
17
+ * mid-compile is never observable.
18
+ */
19
+ export function compileIntent(intent, baseline) {
20
+ const changes = [];
21
+ const skipped = [];
22
+ const capabilities = orderedCapabilities();
23
+ const emit = {
24
+ change: (tool, args, resource, reason) => changes.push({ tool, args, resource, reason }),
25
+ skip: (resource, reason) => skipped.push({ resource, reason }),
26
+ };
27
+ const stateOf = (code) => baseline.models.find((entry) => entry.code === code) ?? absentModel(code);
28
+ // A declared-absent model only ever plans its own deletion, so restrict it to
29
+ // the model capability while keeping capability-major ordering.
30
+ const activeIds = new Map(intent.models.map((model) => [model.code, new Set(capabilitiesForModel(model).map((capability) => capability.id))]));
31
+ for (const capability of capabilities) {
32
+ capability.planRoot?.(intent, baseline, emit);
33
+ for (const model of intent.models)
34
+ if (activeIds.get(model.code).has(capability.id))
35
+ capability.plan?.(model, stateOf(model.code), emit);
36
+ }
37
+ const expectedReadback = intent.models.map((model) => {
38
+ const requestedCodes = model.fields.filter((field) => field.absent !== true).map((field) => field.code);
39
+ const wantsForm = model.form?.publish === true;
40
+ const formFieldCodes = model.form?.fieldCodes ?? requestedCodes;
41
+ return {
42
+ model: model.code,
43
+ publishedFieldCodes: requestedCodes,
44
+ formPublished: wantsForm,
45
+ frontend: { formBoundFieldCodes: wantsForm ? formFieldCodes : [], listviewColumns: model.listview?.fieldCodes ?? [] },
46
+ ...(model.rules?.length ? { rules: model.rules.map((rule) => ({ code: rule.code, enabled: rule.enabled })) } : {}),
47
+ ...(model.datarules?.some((rule) => rule.absent !== true) ? { datarules: model.datarules.filter((rule) => rule.absent !== true).map((rule) => ({ propertyCode: rule.propertyCode, dataRuleType: rule.dataRuleType })) } : {}),
48
+ ...(model.workflow ? { workflow: { code: model.workflow.code, published: model.workflow.publish } } : {}),
49
+ ...(model.onlinejs ? { onlinejs: { published: model.onlinejs.publish } } : {}),
50
+ };
51
+ });
52
+ const expectedServices = (intent.services ?? []).map((service) => ({
53
+ service: service.code,
54
+ adapterCode: service.adapterCode,
55
+ ...(service.method ? { methodCode: service.method.code } : {}),
56
+ }));
57
+ const expectedReports = (intent.reports ?? []).map((report) => ({
58
+ report: report.code,
59
+ appCode: report.appCode,
60
+ chartUuids: (report.charts ?? []).map((chart) => chart.uuid),
61
+ }));
62
+ const tools = new Set(changes.map((change) => change.tool));
63
+ // Spec 107 story 10: destructive changes are explicitly marked. Any `*.delete`
64
+ // is destructive and wins over the lifecycle/additive classification.
65
+ const deletes = changes.filter((change) => change.tool.endsWith(".delete"));
66
+ const riskClass = changes.length === 0
67
+ ? "none"
68
+ : deletes.length > 0
69
+ ? "destructive"
70
+ : tools.has("form.publish") || tools.has("form.draft") || tools.has("field.publish") || tools.has("rule.enable") || tools.has("workflow.publish") || tools.has("onlinejs.compose")
71
+ ? "lifecycle"
72
+ : "additive";
73
+ // The plan's cleanup list makes every declared deletion visible before
74
+ // approval. It only ever names resources the intent explicitly declared
75
+ // absent — the ensure-present ownership boundary is unchanged.
76
+ const cleanup = deletes.map((change) => ({ resource: change.resource, action: change.tool }));
77
+ return { changes, skipped, riskClass, expectedReadback, expectedServices, expectedReports, cleanup };
78
+ }
79
+ //# sourceMappingURL=compiler.js.map
@@ -0,0 +1,129 @@
1
+ import { capabilitiesForModel, orderedCapabilities } from "./capabilities/index.js";
2
+ import { describe, makeValidation, pushError } from "./validation.js";
3
+ import { parseYaml, toPlain, YamlSyntaxError } from "./yaml.js";
4
+ /**
5
+ * Ticket 117 — the declarative intent DSL (v1) and its offline semantic
6
+ * validation. The DSL expresses business intent for the Configuration
7
+ * capabilities the intent face supports; it deliberately does NOT speak wire
8
+ * shapes — the capabilities own DTO defaults, ordering, and lifecycle.
9
+ *
10
+ * Ticket 124 — the assembler is now a thin registry walk: it owns the root
11
+ * shape (intentVersion / models) and hands each model node to every
12
+ * Configuration capability (src/intent/capabilities) for its slice. Adding a
13
+ * capability no longer edits this file.
14
+ *
15
+ * Every validation error carries the source location of the offending key
16
+ * so an agent can fix the file without guessing.
17
+ */
18
+ export const INTENT_VERSION = 1;
19
+ export class IntentInvalidError extends Error {
20
+ errors;
21
+ constructor(errors) {
22
+ super(errors[0]?.message ?? "intent invalid");
23
+ this.errors = errors;
24
+ }
25
+ }
26
+ /** Validate a located YAML tree against the intent DSL v1; returns the normalized intent or throws. */
27
+ export function validateIntentTree(tree) {
28
+ const validation = makeValidation(tree);
29
+ if (tree.kind !== "map") {
30
+ pushError(validation, "intent-root-not-a-map", "intent 文件顶层必须是映射(intentVersion / models)", "", "value");
31
+ throw new IntentInvalidError(validation.errors);
32
+ }
33
+ const entryOf = (key) => tree.entries.find((entry) => entry.key === key)?.value;
34
+ for (const entry of tree.entries) {
35
+ if (!["intentVersion", "models", "services", "reports"].includes(entry.key))
36
+ pushError(validation, "intent-key-unknown", `顶层不支持键 "${entry.key}"(只接受 intentVersion / models / services / reports)`, entry.key);
37
+ }
38
+ const versionNode = entryOf("intentVersion");
39
+ if (versionNode === undefined) {
40
+ pushError(validation, "intent-version-missing", "缺少 intentVersion(当前支持 1)", "models", "value");
41
+ }
42
+ else if (versionNode.kind !== "scalar" || typeof versionNode.value !== "number" || versionNode.value !== INTENT_VERSION) {
43
+ pushError(validation, "intent-version-unsupported", `intentVersion 必须是 ${INTENT_VERSION}(得到 ${describe(versionNode)})`, "intentVersion", "value");
44
+ throw new IntentInvalidError(validation.errors);
45
+ }
46
+ const rootDraft = { services: [], reports: [] };
47
+ const capabilities = orderedCapabilities();
48
+ for (const capability of capabilities)
49
+ capability.validateRoot?.(tree, rootDraft, validation);
50
+ const modelsNode = entryOf("models");
51
+ const hasRootResources = rootDraft.services.length > 0 || rootDraft.reports.length > 0;
52
+ if (modelsNode === undefined) {
53
+ // Ticket 128/r1-report: a root-only intent (services[]/reports[]) is valid;
54
+ // models[] is required only when the intent carries no root-scoped resource.
55
+ if (!hasRootResources)
56
+ pushError(validation, "intent-models-missing", "缺少 models(至少声明一个模型;或声明 services / reports)", "models", "value");
57
+ if (validation.errors.length > 0)
58
+ throw new IntentInvalidError(validation.errors);
59
+ return { intentVersion: INTENT_VERSION, models: [], ...(rootDraft.services.length > 0 ? { services: rootDraft.services } : {}), ...(rootDraft.reports.length > 0 ? { reports: rootDraft.reports } : {}) };
60
+ }
61
+ if (modelsNode.kind !== "seq") {
62
+ pushError(validation, "intent-models-not-a-list", `models 必须是列表(得到 ${describe(modelsNode)})`, "models", "value");
63
+ throw new IntentInvalidError(validation.errors);
64
+ }
65
+ if (modelsNode.items.length === 0) {
66
+ pushError(validation, "intent-models-empty", "models 不能为空——空意图没有可交付内容", "models", "value");
67
+ throw new IntentInvalidError(validation.errors);
68
+ }
69
+ const seenModelCodes = new Set();
70
+ const models = [];
71
+ modelsNode.items.forEach((modelNode, modelIndex) => {
72
+ const base = `models[${modelIndex}]`;
73
+ if (modelNode.kind !== "map") {
74
+ pushError(validation, "intent-model-not-a-map", `模型项必须是映射(得到 ${describe(modelNode)})`, base, "value");
75
+ return;
76
+ }
77
+ const draft = { code: "", name: "", identityValid: false, fields: [], rules: [], datarules: [] };
78
+ const ctx = { ...validation, base, seenModelCodes };
79
+ // A declared-absent model is a deletion target: validate only the model
80
+ // capability, so a stray shape key is refused once (by the model capability)
81
+ // without a cascade of field/form errors.
82
+ const absentNode = modelNode.entries.find((entry) => entry.key === "absent")?.value;
83
+ const absentFlag = absentNode?.kind === "scalar" && absentNode.value === true;
84
+ for (const capability of capabilitiesForModel({ absent: absentFlag }))
85
+ capability.validate?.(modelNode, draft, ctx);
86
+ if (!draft.identityValid)
87
+ return;
88
+ models.push({
89
+ code: draft.code,
90
+ name: draft.name,
91
+ ...(draft.absent ? { absent: true } : {}),
92
+ fields: draft.fields,
93
+ ...(draft.form ? { form: draft.form } : {}),
94
+ ...(draft.listview ? { listview: draft.listview } : {}),
95
+ ...(draft.rules.length > 0 ? { rules: draft.rules } : {}),
96
+ ...(draft.datarules.length > 0 ? { datarules: draft.datarules } : {}),
97
+ ...(draft.workflow ? { workflow: draft.workflow } : {}),
98
+ ...(draft.onlinejs ? { onlinejs: draft.onlinejs } : {}),
99
+ });
100
+ });
101
+ if (validation.errors.length > 0)
102
+ throw new IntentInvalidError(validation.errors);
103
+ return { intentVersion: INTENT_VERSION, models, ...(rootDraft.services.length > 0 ? { services: rootDraft.services } : {}), ...(rootDraft.reports.length > 0 ? { reports: rootDraft.reports } : {}) };
104
+ }
105
+ /** Parse + validate an intent YAML document (offline, zero environment contact). */
106
+ export function parseIntent(text) {
107
+ const tree = parseYaml(text);
108
+ const intent = validateIntentTree(tree);
109
+ return { intent, tree };
110
+ }
111
+ /** Canonical JSON (sorted keys) — the deterministic serialization for hashes. */
112
+ export function canonicalJson(value) {
113
+ return JSON.stringify(sortValue(value));
114
+ }
115
+ function sortValue(value) {
116
+ if (Array.isArray(value))
117
+ return value.map(sortValue);
118
+ if (value !== null && typeof value === "object")
119
+ return Object.fromEntries(Object.entries(value)
120
+ .sort(([a], [b]) => a.localeCompare(b))
121
+ .map(([key, entry]) => [key, sortValue(entry)]));
122
+ return value;
123
+ }
124
+ /** The plain (position-free) value of a parsed intent tree — used by tests and fixtures. */
125
+ export function plainOf(tree) {
126
+ return toPlain(tree);
127
+ }
128
+ export { YamlSyntaxError };
129
+ //# sourceMappingURL=dsl.js.map
@@ -0,0 +1,63 @@
1
+ import { sha256 } from "../support.js";
2
+ import { canonicalJson } from "./dsl.js";
3
+ import { baselineFingerprintOf } from "./baseline.js";
4
+ /**
5
+ * Ticket 118 — the immutable intent plan: the approval object of the
6
+ * declarative face (Spec 107). It pins environment identity, the baseline
7
+ * fingerprint it was computed against, the intent hash, and the exact
8
+ * changes. planHash covers EVERYTHING (canonical JSON, no timestamps) so
9
+ * what a human approves is byte-verifiable at apply time (ticket 119): the
10
+ * planHash IS the approval anchor.
11
+ */
12
+ export const INTENT_PLAN_SCHEMA = "omc-intent-plan-v1";
13
+ export function intentHashOf(intent) {
14
+ return sha256({ scope: "omc-intent-v1", intent: JSON.parse(canonicalJson(intent)) });
15
+ }
16
+ /** Compute the plan's hash over everything except the hash itself. */
17
+ export function planHashOf(plan) {
18
+ return sha256({ scope: INTENT_PLAN_SCHEMA, plan: JSON.parse(canonicalJson(plan)) });
19
+ }
20
+ export function buildPlan(input) {
21
+ const plan = {
22
+ schema: INTENT_PLAN_SCHEMA,
23
+ intentVersion: input.intent.intentVersion,
24
+ compiler: input.compiler,
25
+ environment: input.environment,
26
+ environmentFingerprint: input.environmentFingerprint,
27
+ baselineFingerprint: baselineFingerprintOf(input.baseline),
28
+ baseline: input.baseline,
29
+ intent: input.intent,
30
+ intentHash: intentHashOf(input.intent),
31
+ changes: input.compiled.changes,
32
+ skipped: input.compiled.skipped,
33
+ riskClass: input.compiled.riskClass,
34
+ expectedReadback: input.compiled.expectedReadback,
35
+ cleanup: input.compiled.cleanup,
36
+ ...(input.compiled.expectedServices.length > 0 ? { expectedServices: input.compiled.expectedServices } : {}),
37
+ ...(input.compiled.expectedReports.length > 0 ? { expectedReports: input.compiled.expectedReports } : {}),
38
+ };
39
+ return { ...plan, planHash: planHashOf(plan) };
40
+ }
41
+ /** Structural check + hash re-verification of a parsed plan file. */
42
+ export function verifyPlan(value) {
43
+ if (value === null || typeof value !== "object" || Array.isArray(value))
44
+ return { ok: false, code: "intent-plan-invalid", reason: "plan 文件必须是 JSON 对象" };
45
+ const plan = value;
46
+ if (plan.schema !== INTENT_PLAN_SCHEMA)
47
+ return { ok: false, code: "intent-plan-invalid", reason: `plan.schema 必须是 ${INTENT_PLAN_SCHEMA}` };
48
+ const required = ["intentVersion", "compiler", "environment", "environmentFingerprint", "baselineFingerprint", "baseline", "intent", "intentHash", "changes", "skipped", "riskClass", "expectedReadback", "cleanup", "planHash"];
49
+ for (const key of required) {
50
+ if (plan[key] === undefined)
51
+ return { ok: false, code: "intent-plan-invalid", reason: `plan 缺少必填字段 ${key}` };
52
+ }
53
+ if (!Array.isArray(plan.changes) || plan.changes.some((change) => typeof change?.tool !== "string"))
54
+ return { ok: false, code: "intent-plan-invalid", reason: "plan.changes 必须是 {tool,args,resource,reason} 数组" };
55
+ if (typeof plan.planHash !== "string" || plan.planHash.length !== 64)
56
+ return { ok: false, code: "intent-plan-invalid", reason: "plan.planHash 必须是 64 位 sha256" };
57
+ const { planHash, ...rest } = plan;
58
+ const recomputed = planHashOf(rest);
59
+ if (recomputed !== planHash)
60
+ return { ok: false, code: "intent-plan-tampered", reason: "plan 哈希不匹配:文件内容与生成时不一致(what you approve must be byte-identical to what executes)" };
61
+ return { ok: true, plan: plan };
62
+ }
63
+ //# sourceMappingURL=plan-file.js.map
@@ -0,0 +1,65 @@
1
+ import { capabilitiesForModel, orderedCapabilities } from "./capabilities/index.js";
2
+ const absentForm = { present: false, published: false, existDraft: false, boundFieldCodes: [] };
3
+ /** Fresh per model: form/listview decoders write into the frontend in place. */
4
+ const newFrontend = () => ({ formBoundFieldCodes: [], listviewPresent: false, listviewColumns: [] });
5
+ /** A model the live read did not cover still compares as fully absent (defensive). */
6
+ function absentLiveModel(code) {
7
+ return { code, name: null, present: false, fields: [], form: absentForm, frontend: newFrontend() };
8
+ }
9
+ /**
10
+ * Decode a baseline into the intent shape (wire ints -> family names). The
11
+ * optional `intent` lets a declared-absent model run ONLY the model capability,
12
+ * mirroring compare (a baseline alone cannot know which models are deleted).
13
+ */
14
+ export function decodeBaseline(baseline, intent) {
15
+ const absentCodes = new Set((intent?.models ?? []).filter((model) => model.absent === true).map((model) => model.code));
16
+ const live = {
17
+ models: baseline.models.map((model) => {
18
+ const draft = {
19
+ code: model.code,
20
+ name: model.name,
21
+ present: model.present,
22
+ fields: [],
23
+ form: absentForm,
24
+ frontend: newFrontend(),
25
+ rules: [],
26
+ datarules: [],
27
+ };
28
+ for (const capability of capabilitiesForModel({ absent: absentCodes.has(model.code) }))
29
+ capability.decode?.(model, draft);
30
+ const { rules, datarules, ...rest } = draft;
31
+ return { ...rest, ...(rules.length > 0 ? { rules } : {}), ...(datarules.length > 0 ? { datarules } : {}) };
32
+ }),
33
+ outsideModels: baseline.outsideModels,
34
+ };
35
+ for (const capability of orderedCapabilities())
36
+ capability.decodeRoot?.(baseline, live);
37
+ return live;
38
+ }
39
+ /** Compare the source intent with the decoded live state. */
40
+ export function compareWithLive(intent, live) {
41
+ const differences = [];
42
+ const ignored = [];
43
+ const emit = {
44
+ difference: (kind, resource, message) => differences.push({ kind, resource, message }),
45
+ ignored: (resource) => ignored.push(resource),
46
+ };
47
+ const capabilities = orderedCapabilities();
48
+ const liveModels = new Map(live.models.map((model) => [model.code, model]));
49
+ for (const model of intent.models) {
50
+ const state = liveModels.get(model.code) ?? absentLiveModel(model.code);
51
+ // A declared-absent model compares only its own presence.
52
+ for (const capability of capabilitiesForModel(model))
53
+ capability.compare?.(model, state, emit);
54
+ }
55
+ for (const capability of capabilities)
56
+ capability.compareRoot?.(intent, live, emit);
57
+ const intentModelCodes = new Set(intent.models.map((model) => model.code));
58
+ for (const state of live.models)
59
+ if (!intentModelCodes.has(state.code))
60
+ ignored.push(`model:${state.code}`);
61
+ for (const code of live.outsideModels)
62
+ ignored.push(`model:${code}`);
63
+ return { equivalent: differences.length === 0, differences, ignoredOutsideIntent: ignored };
64
+ }
65
+ //# sourceMappingURL=readback.js.map
@@ -0,0 +1,158 @@
1
+ /**
2
+ * P0 friction fix (3-round dogfood 2026-09-29): the intent YAML had no
3
+ * authoritative discovery face — `omc intent --help` lists four subcommands and
4
+ * `omc describe intent` did not exist, so callers had to reconstruct the key set,
5
+ * the comparison operators, the choice-family shape and the relation shape from
6
+ * prose + `validate` trial-and-error. This module is that face: one offline
7
+ * projection of the DSL (key names, allowed values, operators, runnable
8
+ * examples) reachable as `omc intent schema` and `omc describe intent`.
9
+ *
10
+ * It is a projection, not a second source of truth: key sets are imported from
11
+ * the capability validators so they cannot drift.
12
+ */
13
+ /** Comparison operators accepted by `actions[].when` (execution gate) and `actions[].filter` (row locator). */
14
+ export const INTENT_CONDITION_OPERATORS = Object.freeze({
15
+ equals: "EQ (equal)",
16
+ notEquals: "NEQ (not equal)",
17
+ gt: "GT (greater than)",
18
+ gte: "GTEQ (greater than or equal)",
19
+ lt: "LT (less than)",
20
+ lte: "LTEQ (less than or equal)",
21
+ from: "dynamic — compare against another FIELD: `from: <fieldCode>` (equivalent to equals a field; only for `filter`)",
22
+ });
23
+ /** A `when` gate leaf compares against a literal; `from` (field-to-field) is only for `filter`. */
24
+ export const INTENT_WHEN_OPERATORS = Object.freeze(["equals", "notEquals", "gt", "gte", "lt", "lte"]);
25
+ const RUNNABLE_EXAMPLES = Object.freeze([
26
+ {
27
+ id: "scalar-numeric-radio",
28
+ title: "标量/数值/RADIO(choices)——type 用族名,NUMERICAL 必带 options.scale,选择族用顶层 choices",
29
+ yaml: [
30
+ "intentVersion: 1",
31
+ "models:",
32
+ " - code: ex_order",
33
+ " name: 订单",
34
+ " fields:",
35
+ " - { code: order_no, name: 订单号, type: SHORT_TEXT }",
36
+ " - { code: amount, name: 金额, type: NUMERICAL, options: { scale: 2 } }",
37
+ " - { code: level, name: 等级, type: RADIO, choices: [A, B, C] } # choices 只用于 RADIO/CHECKBOX/DROPDOWN_BOX/DROPDOWN_MULTI_BOX",
38
+ " form: { publish: true }",
39
+ " listview: { fieldCodes: [order_no, amount, level] }",
40
+ "",
41
+ ].join("\n"),
42
+ },
43
+ {
44
+ id: "relation-work-sheet",
45
+ title: "关联字段 WORK_SHEET(relativeCode 指向目标模型,displayField 用 options;`mappings` 在原始 field.create 面是空字符串)",
46
+ yaml: [
47
+ "intentVersion: 1",
48
+ "models:",
49
+ " - code: ex_supplier",
50
+ " name: 供应商",
51
+ " fields:",
52
+ " - { code: sup_name, name: 名称, type: SHORT_TEXT }",
53
+ " form: { publish: true }",
54
+ " - code: ex_contract",
55
+ " name: 合同",
56
+ " fields:",
57
+ " - code: supplier",
58
+ " name: 供应商",
59
+ " type: WORK_SHEET",
60
+ " relativeCode: ex_supplier # 目标模型 code",
61
+ " options: { displayField: sup_name } # 显示字段",
62
+ " form: { publish: true }",
63
+ "",
64
+ ].join("\n"),
65
+ },
66
+ {
67
+ id: "rule-actions-when-filter",
68
+ title: "规则紧凑 actions:一条规则可挂多个异构 action;when=执行门(满足才执行,只支持字面量比较),filter=定位要改的行(省略则默认当前行 id=id),跨模型回写用 filter from",
69
+ yaml: [
70
+ "intentVersion: 1",
71
+ "models:",
72
+ " - code: ex_order",
73
+ " name: 订单",
74
+ " fields:",
75
+ " - { code: amount, name: 金额, type: NUMERICAL, options: { scale: 2 } }",
76
+ " - { code: status, name: 状态, type: SHORT_TEXT }",
77
+ " - { code: supplier, name: 供应商, type: WORK_SHEET, relativeCode: ex_supplier }",
78
+ " rules:",
79
+ " - code: Create",
80
+ " name: 新增分级",
81
+ " enabled: true",
82
+ " actions:",
83
+ " # 同一规则内多个 action:按数组顺序编译为串行节点",
84
+ " - type: UPDATE",
85
+ " target: ex_order",
86
+ " when: [{ field: amount, gte: 100000 }] # 执行门:满足才执行(gte/lte 合法)",
87
+ " set: [{ field: status, value: 大额 }] # 省略 filter -> 只改当前记录",
88
+ " - type: UPDATE",
89
+ " target: ex_order",
90
+ " when: [{ field: amount, lt: 100000 }]",
91
+ " set: [{ field: status, value: 小额 }]",
92
+ " - type: UPDATE",
93
+ " target: ex_supplier # 跨模型回写",
94
+ " filter: [{ field: id, from: supplier }] # 定位目标行:目标 id = 当前记录的 supplier",
95
+ " set: [{ field: total_amount, from: amount }]",
96
+ "",
97
+ ].join("\n"),
98
+ },
99
+ ]);
100
+ /**
101
+ * The intent-face DSL schema (offline). Key sets mirror the validators; this is
102
+ * what `omc intent schema` / `omc describe intent` return.
103
+ */
104
+ export function intentSchemaBlock() {
105
+ return {
106
+ status: "ok",
107
+ intentVersion: 1,
108
+ topLevel: { required: ["intentVersion", "models"], optional: ["services"], note: "intentVersion 必须是 1" },
109
+ models: {
110
+ required: ["code", "fields"],
111
+ optional: ["name", "absent", "form", "listview", "rules", "datarules", "workflow", "onlinejs"],
112
+ absent: "absent:true 删除整个模型(riskClass=destructive);显式缺席是唯一删除语义",
113
+ },
114
+ fields: {
115
+ required: ["code(顶层;子列同为 code)", "type(字段族名)"],
116
+ optional: ["name", "choices", "subFields", "options", "relativeCode", "displayField", "absent"],
117
+ notes: [
118
+ "type 用字段族名(见 fieldFamilies);NUMERICAL 必带 options.scale",
119
+ "choices 只用于选择族 RADIO / CHECKBOX / DROPDOWN_BOX / DROPDOWN_MULTI_BOX(顶层 choices,不是 options.items/choices)",
120
+ "WORK_SHEET / MULT_WORK_SHEET 用顶层 relativeCode=<目标模型 code>;显示列用 options.displayField",
121
+ "CHILD_TABLE 用 subFields[{code,name,type[,choices]}]; 子列族仅标量",
122
+ "门户层条件(必填/只读/显示)写在 options:options.requiredFormula / readonlyFormula / readonlyCondition / displayFormula",
123
+ ],
124
+ },
125
+ form: { required: ["publish"], optional: ["fieldCodes"] },
126
+ listview: { required: [], optional: ["code", "name", "fieldCodes", "absent"], note: "intent 只声明列;业务筛选/排序/按钮 apply 后用 omc call listview.configure 补" },
127
+ datarules: { required: ["propertyCode", "dataRuleType"], optional: ["options", "checkType", "childRule", "name", "sortKey", "absent"], note: "type3/4/5 不驱动门户;门户条件走 fields[].options" },
128
+ rules: {
129
+ required: ["code", "name", "enabled"],
130
+ optional: ["bizRuleType", "customTrigger", "actions", "nodeGraph"],
131
+ note: "actions(紧凑 UPDATE)与 nodeGraph 二选一;非 UPDATE 节点用 nodeGraph",
132
+ actionLeaf: {
133
+ type: "UPDATE(目前唯一)",
134
+ target: "目标模型 code(省略=当前模型)",
135
+ when: "执行门 dataCondition:[{field, <op>: <literal>}],op ∈ " + INTENT_WHEN_OPERATORS.join("/"),
136
+ filter: "定位 filterCondition:[{field, <op>: <literal>}] 或 [{field, from: <当前字段>}];省略=只改当前行(id=id)",
137
+ set: "[{field, value: <literal>} | {field, from: <当前字段>}]",
138
+ },
139
+ },
140
+ conditionOperators: INTENT_CONDITION_OPERATORS,
141
+ workflow: { required: ["code", "name", "publish"], optional: ["template", "bindRules"], note: "bindRules [{handler: start|cancel|activate|end, ruleCodes:[...]}];流程事件只绑自定义规则" },
142
+ onlinejs: { required: ["source", "publish"], optional: ["sheetCode", "replace"], note: "replace:true 覆盖;merge 时 source 只给生命周期片段,不能是完整 (function(form){...})" },
143
+ services: { required: ["code", "name", "adapterCode", "serviceCategoryId", "serviceConfig"], optional: ["method"], note: "自定义/INTERNAL 适配器走二开扩展包导入,不在声明式写面" },
144
+ examples: RUNNABLE_EXAMPLES.map((example) => ({ id: example.id, title: example.title })),
145
+ exampleHint: "取可运行 YAML:omc describe intent --example <id>",
146
+ versionNotice: "intent 只覆盖声明式子集(capabilities 的 intent: partial);未覆盖面用 omc call 补齐。",
147
+ };
148
+ }
149
+ /** A single runnable example for `describe intent --example <id>`. */
150
+ export function intentExample(id) {
151
+ const example = RUNNABLE_EXAMPLES.find((entry) => entry.id === id);
152
+ return example ? { status: "ok", id: example.id, title: example.title, yaml: example.yaml } : undefined;
153
+ }
154
+ /** All example ids (for near-miss hints). */
155
+ export function intentExampleIds() {
156
+ return RUNNABLE_EXAMPLES.map((example) => example.id);
157
+ }
158
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,30 @@
1
+ import { locateKey, nodeAt } from "./yaml.js";
2
+ export function makeValidation(tree) {
3
+ const errors = [];
4
+ const fallback = { line: 1, column: 1 };
5
+ return {
6
+ errors,
7
+ locateKey: (path) => locateKey(tree, path) ?? fallback,
8
+ locateValue: (path) => {
9
+ const node = nodeAt(tree, path);
10
+ return node === undefined ? (locateKey(tree, path) ?? fallback) : { line: node.line, column: node.column };
11
+ },
12
+ };
13
+ }
14
+ export function pushError(validation, code, message, path, pointer = "key", extra) {
15
+ const located = pointer === "key" ? validation.locateKey(path) : validation.locateValue(path);
16
+ validation.errors.push({ code, message, path, line: located.line, column: located.column, ...(extra?.validation ? { validation: extra.validation } : {}) });
17
+ }
18
+ export const isScalarString = (node) => node !== undefined && node.kind === "scalar" && typeof node.value === "string" && node.value.length > 0;
19
+ export function describe(node) {
20
+ if (node === undefined)
21
+ return "缺失";
22
+ if (node.kind === "map")
23
+ return "映射";
24
+ if (node.kind === "seq")
25
+ return "列表";
26
+ if (node.value === null)
27
+ return "null";
28
+ return `"${String(node.value)}"(${typeof node.value})`;
29
+ }
30
+ //# sourceMappingURL=validation.js.map