octwin-cli 0.7.2 → 0.8.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.
@@ -58,6 +58,23 @@ export function loadAllowedRenderKeys(packDir) {
58
58
  }
59
59
  return { lookup, keys: out, nested };
60
60
  }
61
+ /** The tap verbs, in the order the platform's compiler picks them. */
62
+ const ON_SELECT_VERBS = ['invoke', 'resume', 'reply', 'agent'];
63
+ /**
64
+ * `{ if, then, else }` — mirrors `isIfFragment` in the platform's
65
+ * `interpreter/render-resolver.ts` and `schemas/render.ts`. Three copies is two too
66
+ * many, but this package is deliberately dependency-free from `src/platform`; the
67
+ * predicate is identical and `render-check.parity.test.ts` is what keeps it so.
68
+ */
69
+ function isIfFragment(v) {
70
+ return !!v && typeof v === 'object' && !Array.isArray(v) &&
71
+ typeof v.if === 'string' &&
72
+ Object.keys(v).every(k => k === 'if' || k === 'then' || k === 'else');
73
+ }
74
+ /** The `on_select` NODE itself — the one place an if-fragment is not evaluated. */
75
+ function isOnSelectPath(at) {
76
+ return at === 'on_select' || at.endsWith('.on_select');
77
+ }
61
78
  /**
62
79
  * Walk parsed YAML for objects carrying `render_intent` and report keys outside
63
80
  * that intent's allowed set. Walks the whole document rather than just `render:`
@@ -81,7 +98,50 @@ export function findRenderKeyViolations(doc, file, allowedByIntent, nestedByInte
81
98
  if (!value || typeof value !== 'object')
82
99
  return;
83
100
  const obj = value;
101
+ // An if-fragment stands in for the value it selects, so check its branches at the SAME
102
+ // path. Mirrors the platform's `checkNested`: without this the wrapper's own keys were
103
+ // reported against the slot it wraps, which rejected the one form that actually works.
104
+ if (isIfFragment(obj) && !isOnSelectPath(at)) {
105
+ for (const b of ['then', 'else']) {
106
+ if (obj[b] !== undefined)
107
+ walkNested(obj[b], intent, at, sets, [...path, b]);
108
+ }
109
+ return;
110
+ }
111
+ // …and at an `on_select` node it is the reported bug, because runtime does not evaluate
112
+ // it there. One sentence naming the two forms that do work.
113
+ if (isIfFragment(obj) && isOnSelectPath(at)) {
114
+ findings.push({
115
+ file, path: [...path], intent, keys: [], allowed: ON_SELECT_VERBS, nestedAt: at,
116
+ note: `render_intent '${intent}' at '${at}': '{ if, then, else }' is not evaluated inside ` +
117
+ `'on_select' — it fails at render time, not at validate. Wrap the WHOLE entry instead ` +
118
+ `({ if, then: { title, on_select }, else: {…} }), or give each row its own on_select in ` +
119
+ `data and point at it (on_select: '$item.on_select').`,
120
+ });
121
+ return;
122
+ }
84
123
  const allowed = sets[at];
124
+ // A missing verb is an absence, not an unknown key, so the key check below is blind to it —
125
+ // and the tap compiler throws on it at render. Only judged where the KB declares the slot,
126
+ // so a KB pulled before on_select was declared simply does not run this.
127
+ if (allowed && isOnSelectPath(at)) {
128
+ const present = ON_SELECT_VERBS.filter(v => obj[v] !== undefined);
129
+ if (present.length === 0) {
130
+ findings.push({
131
+ file, path: [...path], intent, keys: [], allowed: ON_SELECT_VERBS, nestedAt: at,
132
+ note: `render_intent '${intent}' at '${at}': 'on_select' needs exactly one of ` +
133
+ `${ON_SELECT_VERBS.join(' / ')} — it has none, so the tap has no target and ` +
134
+ `fails at render time.`,
135
+ });
136
+ }
137
+ else if (present.length > 1) {
138
+ findings.push({
139
+ file, path: [...path], intent, keys: [], allowed: ON_SELECT_VERBS, nestedAt: at,
140
+ note: `render_intent '${intent}' at '${at}': 'on_select' has more than one verb ` +
141
+ `(${present.join(', ')}); only '${present[0]}' would run. Keep one.`,
142
+ });
143
+ }
144
+ }
85
145
  if (allowed) {
86
146
  const bad = Object.keys(obj).filter(k => !allowed.includes(k));
87
147
  if (bad.length)
@@ -126,6 +186,10 @@ export function findRenderKeyViolations(doc, file, allowedByIntent, nestedByInte
126
186
  /** One-line human message per finding. */
127
187
  export function describeRenderFinding(f) {
128
188
  const at = f.line != null ? `:${f.line}` : (f.path.length ? ` (${f.path.join('.')})` : '');
189
+ // A pre-worded finding (absent verb, if-fragment on a tap payload) carries its own sentence;
190
+ // the key-list template below cannot say either one.
191
+ if (f.note)
192
+ return `${f.file}${at}: ${f.note}`;
129
193
  if (f.keys.length === 0) {
130
194
  return `${f.file}${at}: unknown render_intent '${f.intent}' — known intents: ${f.allowed.join(', ')}`;
131
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {