@objectstack/lint 17.1.0 → 17.2.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.
@@ -35,7 +35,13 @@ interface AuthoringFinding {
35
35
  rule: string;
36
36
  /** Human-readable location, e.g. `object "leave_request"`. */
37
37
  where: string;
38
- /** Config path, e.g. `objects[3].sharingModel`. */
38
+ /**
39
+ * Config path, e.g. `objects[3].sharingModel`. Positional as RULES emit it;
40
+ * on the runtime gate's wire surface the top-level collection index of a
41
+ * collection-resident finding is rewritten to the entry's NAME
42
+ * (`objects.acme_invoice.sharingModel`) — see `nameKeyFindingPath` in
43
+ * `runtime-gate.ts` (#10064).
44
+ */
39
45
  path: string;
40
46
  /** What is wrong. */
41
47
  message: string;
@@ -117,6 +123,19 @@ type AuthoringRuleInputTier = 'normalized' | 'parsed';
117
123
  interface AuthoringRuleContext {
118
124
  /** ADR-0080 SDUI component manifest, when the project ships one. */
119
125
  sduiManifest?: unknown;
126
+ /**
127
+ * [#9313] The singular metadata type of the per-write snapshot being judged
128
+ * — set by the runtime publish gate (`runtime-gate.ts`) on every gated
129
+ * write, ABSENT on the three CLI commands (`runAuthoringRules` never sets
130
+ * it). Exists for the one entry that is itself a registry: the
131
+ * reference-integrity suite dispatches its MEMBERS by this
132
+ * (`ReferenceIntegrityRule.runtimeTypes`), because the entry-level
133
+ * `runtimeTypes` can only say which writes reach the suite, not which
134
+ * members can judge a partial per-write snapshot without inventing
135
+ * findings. No other rule reads it, and none should without the same
136
+ * argument.
137
+ */
138
+ runtimeWriteType?: string;
120
139
  }
121
140
  interface AuthoringRule {
122
141
  /** The exported function's name — the id the wiring guard asserts on. */
@@ -35,7 +35,13 @@ interface AuthoringFinding {
35
35
  rule: string;
36
36
  /** Human-readable location, e.g. `object "leave_request"`. */
37
37
  where: string;
38
- /** Config path, e.g. `objects[3].sharingModel`. */
38
+ /**
39
+ * Config path, e.g. `objects[3].sharingModel`. Positional as RULES emit it;
40
+ * on the runtime gate's wire surface the top-level collection index of a
41
+ * collection-resident finding is rewritten to the entry's NAME
42
+ * (`objects.acme_invoice.sharingModel`) — see `nameKeyFindingPath` in
43
+ * `runtime-gate.ts` (#10064).
44
+ */
39
45
  path: string;
40
46
  /** What is wrong. */
41
47
  message: string;
@@ -117,6 +123,19 @@ type AuthoringRuleInputTier = 'normalized' | 'parsed';
117
123
  interface AuthoringRuleContext {
118
124
  /** ADR-0080 SDUI component manifest, when the project ships one. */
119
125
  sduiManifest?: unknown;
126
+ /**
127
+ * [#9313] The singular metadata type of the per-write snapshot being judged
128
+ * — set by the runtime publish gate (`runtime-gate.ts`) on every gated
129
+ * write, ABSENT on the three CLI commands (`runAuthoringRules` never sets
130
+ * it). Exists for the one entry that is itself a registry: the
131
+ * reference-integrity suite dispatches its MEMBERS by this
132
+ * (`ReferenceIntegrityRule.runtimeTypes`), because the entry-level
133
+ * `runtimeTypes` can only say which writes reach the suite, not which
134
+ * members can judge a partial per-write snapshot without inventing
135
+ * findings. No other rule reads it, and none should without the same
136
+ * argument.
137
+ */
138
+ runtimeWriteType?: string;
120
139
  }
121
140
  interface AuthoringRule {
122
141
  /** The exported function's name — the id the wiring guard asserts on. */
package/dist/runtime.cjs CHANGED
@@ -2190,6 +2190,26 @@ function validateSearchableFields(stack) {
2190
2190
  if (!isRec3(view)) continue;
2191
2191
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
2192
2192
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
2193
+ if (view.viewKind === "list" && !isRec3(view.config)) {
2194
+ check(
2195
+ view.searchableFields,
2196
+ listViewObject(view) ?? viewObject,
2197
+ `view "${viewLabel2}" (flattened list overlay)`,
2198
+ `views[${vi}].searchableFields`,
2199
+ "list-view searchableFields",
2200
+ "narrowing"
2201
+ );
2202
+ }
2203
+ if (view.viewKind === "list" && isRec3(view.config)) {
2204
+ check(
2205
+ view.config.searchableFields,
2206
+ listViewObject(view.config) ?? viewObject,
2207
+ `view "${viewLabel2}" (ViewItem record)`,
2208
+ `views[${vi}].config.searchableFields`,
2209
+ "list-view searchableFields",
2210
+ "narrowing"
2211
+ );
2212
+ }
2193
2213
  if (isRec3(view.list)) {
2194
2214
  check(
2195
2215
  view.list.searchableFields,
@@ -2225,6 +2245,7 @@ function listViewObject(listView) {
2225
2245
  var import_data8 = require("@objectstack/spec/data");
2226
2246
  var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2227
2247
  var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2248
+ var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
2228
2249
  function isRec4(v) {
2229
2250
  return !!v && typeof v === "object" && !Array.isArray(v);
2230
2251
  }
@@ -2287,7 +2308,7 @@ function distance3(a, b) {
2287
2308
  }
2288
2309
  return prev[n];
2289
2310
  }
2290
- function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
2311
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
2291
2312
  const findings = [];
2292
2313
  if (declared === void 0 || declared === null) return findings;
2293
2314
  if (!objectName) return findings;
@@ -2295,10 +2316,23 @@ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path,
2295
2316
  const target = fieldsByObject.get(objectName);
2296
2317
  if (!target) return findings;
2297
2318
  const known = target.names;
2319
+ const anchors = unprovisionedAnchors?.get(objectName);
2298
2320
  for (const key of readSortKeys(declared)) {
2299
2321
  const name = key.field;
2300
2322
  const head = name.split(".")[0];
2301
- if (SYSTEM_FIELDS.has(head)) continue;
2323
+ if (SYSTEM_FIELDS.has(head)) {
2324
+ if (!name.includes(".") && anchors?.has(head)) {
2325
+ findings.push({
2326
+ severity: "warning",
2327
+ rule: SORT_FIELD_UNPROVISIONED,
2328
+ where,
2329
+ path: `${path}${key.at}`,
2330
+ message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
2331
+ hint: unprovisionedAnchorHint(objectName, name)
2332
+ });
2333
+ }
2334
+ continue;
2335
+ }
2302
2336
  if (!known.has(head)) {
2303
2337
  const dotted = name.includes(".");
2304
2338
  findings.push({
@@ -2331,9 +2365,18 @@ function validateSortableFields(stack) {
2331
2365
  if (!isRec4(stack)) return findings;
2332
2366
  const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2333
2367
  const fieldsByObject = indexObjectSearchTargets(stack);
2368
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2334
2369
  const check = (declared, objectName, where, path, subject) => {
2335
2370
  findings.push(
2336
- ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
2371
+ ...checkSortDeclaration(
2372
+ declared,
2373
+ objectName,
2374
+ fieldsByObject,
2375
+ where,
2376
+ path,
2377
+ subject,
2378
+ unprovisionedAnchors
2379
+ )
2337
2380
  );
2338
2381
  };
2339
2382
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2362,6 +2405,24 @@ function validateSortableFields(stack) {
2362
2405
  if (!isRec4(view)) continue;
2363
2406
  const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2364
2407
  const viewObject = strName4(view.objectName) ?? strName4(view.object);
2408
+ if (view.viewKind === "list" && !isRec4(view.config)) {
2409
+ check(
2410
+ view.sort,
2411
+ listViewObject2(view) ?? viewObject,
2412
+ `view "${viewLabel2}" (flattened list overlay)`,
2413
+ `views[${vi}].sort`,
2414
+ "list-view sort"
2415
+ );
2416
+ }
2417
+ if (view.viewKind === "list" && isRec4(view.config)) {
2418
+ check(
2419
+ view.config.sort,
2420
+ listViewObject2(view.config) ?? viewObject,
2421
+ `view "${viewLabel2}" (ViewItem record)`,
2422
+ `views[${vi}].config.sort`,
2423
+ "list-view sort"
2424
+ );
2425
+ }
2365
2426
  if (isRec4(view.list)) {
2366
2427
  check(
2367
2428
  view.list.sort,
@@ -4542,6 +4603,38 @@ function validateAiAgentAuthoring(stack) {
4542
4603
  // src/validate-hook-body-writes.ts
4543
4604
  var import_node_module = require("module");
4544
4605
  var import_shared = require("@objectstack/spec/shared");
4606
+
4607
+ // src/checked-parse.ts
4608
+ function createSourceFileChecked(tsc, fileName, source, options) {
4609
+ const sourceFile = tsc.createSourceFile(
4610
+ fileName,
4611
+ source,
4612
+ options.target,
4613
+ options.setParentNodes,
4614
+ options.scriptKind
4615
+ );
4616
+ const diagnostics = sourceFile.parseDiagnostics;
4617
+ if (!diagnostics || diagnostics.length === 0) return { sourceFile };
4618
+ const first = diagnostics[0];
4619
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
4620
+ const offset = options.synthesizedLinesBefore ?? 0;
4621
+ return {
4622
+ sourceFile,
4623
+ failure: {
4624
+ message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
4625
+ line: Math.max(1, line + 1 - offset),
4626
+ column: character + 1,
4627
+ count: diagnostics.length
4628
+ }
4629
+ };
4630
+ }
4631
+ function describeParseFailure(failure) {
4632
+ const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
4633
+ return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
4634
+ }
4635
+ var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
4636
+
4637
+ // src/validate-hook-body-writes.ts
4545
4638
  var import_meta = {};
4546
4639
  var cachedTs = null;
4547
4640
  function loadTypeScript() {
@@ -4557,6 +4650,7 @@ function loadTypeScript() {
4557
4650
  return cachedTs;
4558
4651
  }
4559
4652
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4653
+ var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
4560
4654
  var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4561
4655
  var HOOK_BODY_WRITE_PATTERNS = [
4562
4656
  {
@@ -4656,23 +4750,23 @@ function judgeableFieldsOf(index, objectName) {
4656
4750
  if (!declared || declared.size === 0) return void 0;
4657
4751
  return declared;
4658
4752
  }
4659
- function extractHookBodyWrites(source) {
4660
- return extractHookBodyWriteSet(source).writes;
4661
- }
4662
4753
  function extractHookBodyWriteSet(source) {
4663
4754
  if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
4664
4755
  return { writes: [], ctxRecordEscapes: false };
4665
4756
  }
4666
4757
  const tsc = loadTypeScript();
4667
- const sf = tsc.createSourceFile(
4758
+ const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
4759
+ tsc,
4668
4760
  "hook-body.ts",
4669
4761
  `async function __body(ctx) {
4670
4762
  ${source}
4671
4763
  }`,
4672
- tsc.ScriptTarget.Latest,
4673
- /* setParentNodes */
4674
- false,
4675
- tsc.ScriptKind.TS
4764
+ {
4765
+ target: tsc.ScriptTarget.Latest,
4766
+ setParentNodes: false,
4767
+ scriptKind: tsc.ScriptKind.TS,
4768
+ synthesizedLinesBefore: 1
4769
+ }
4676
4770
  );
4677
4771
  const writes = [];
4678
4772
  const recordRefs = [];
@@ -4769,7 +4863,8 @@ ${source}
4769
4863
  visit(sf);
4770
4864
  return {
4771
4865
  writes,
4772
- ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
4866
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
4867
+ ...parseFailure ? { parseFailure } : {}
4773
4868
  };
4774
4869
  }
4775
4870
  function validateHookBodyWrites(stack) {
@@ -4783,11 +4878,22 @@ function validateHookBodyWrites(stack) {
4783
4878
  if (!isRec15(body) || body.language !== "js") return;
4784
4879
  const source = body.source;
4785
4880
  if (typeof source !== "string" || source.trim() === "") return;
4786
- const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4881
+ const extracted = extractHookBodyWriteSet(source);
4882
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4883
+ if (extracted.parseFailure) {
4884
+ findings.push({
4885
+ severity: "warning",
4886
+ rule: HOOK_BODY_SOURCE_UNPARSEABLE,
4887
+ where: `hook "${hookName}" \u203A body`,
4888
+ path: `hooks[${hookIndex}].body.source`,
4889
+ message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
4890
+ hint: PARSE_FAILURE_HINT
4891
+ });
4892
+ }
4893
+ const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4787
4894
  if (writes.length === 0) return;
4788
4895
  objectFields ?? (objectFields = indexObjectFields2(stack));
4789
4896
  anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4790
- const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4791
4897
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4792
4898
  (o) => typeof o === "string" && o.trim() !== ""
4793
4899
  );
@@ -4872,6 +4978,7 @@ function fixHint(field, declared) {
4872
4978
  var import_shared2 = require("@objectstack/spec/shared");
4873
4979
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4874
4980
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4981
+ var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
4875
4982
  var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4876
4983
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4877
4984
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
@@ -4929,11 +5036,21 @@ function validateActionBodyWrites(stack) {
4929
5036
  let anchors = null;
4930
5037
  for (const site of sites) {
4931
5038
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4932
- const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5039
+ const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
4933
5040
  const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
4934
5041
  const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
4935
- if (writes.length === 0 && recordWrites.length === 0) continue;
4936
5042
  const where = `action "${site.name}" \u203A body`;
5043
+ if (parseFailure) {
5044
+ findings.push({
5045
+ severity: "warning",
5046
+ rule: ACTION_BODY_SOURCE_UNPARSEABLE,
5047
+ where,
5048
+ path: site.path,
5049
+ message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
5050
+ hint: PARSE_FAILURE_HINT
5051
+ });
5052
+ }
5053
+ if (writes.length === 0 && recordWrites.length === 0) continue;
4937
5054
  if (recordWrites.length > 0 && !ctxRecordEscapes) {
4938
5055
  const reportedFields = /* @__PURE__ */ new Set();
4939
5056
  for (const w of recordWrites) {
@@ -5306,6 +5423,7 @@ function filterAttrValue(tsc, sf, attr) {
5306
5423
  };
5307
5424
  return perPosition(init.expression);
5308
5425
  }
5426
+ var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
5309
5427
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
5310
5428
  var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
5311
5429
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
@@ -5634,11 +5752,20 @@ function validateReactPageProps(stack) {
5634
5752
  if (typeof source !== "string" || source.trim() === "") continue;
5635
5753
  const name = String(page.name ?? `#${p}`);
5636
5754
  const tsc = loadTypeScript2();
5637
- let sf;
5638
- try {
5639
- sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
5640
- } catch {
5641
- continue;
5755
+ const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
5756
+ target: tsc.ScriptTarget.Latest,
5757
+ setParentNodes: true,
5758
+ scriptKind: tsc.ScriptKind.TSX
5759
+ });
5760
+ if (failure) {
5761
+ findings.push({
5762
+ severity: "warning",
5763
+ rule: REACT_PAGE_SOURCE_UNPARSEABLE,
5764
+ where: `page "${name}"`,
5765
+ path: `pages[${p}].source`,
5766
+ message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
5767
+ hint: PARSE_FAILURE_HINT
5768
+ });
5642
5769
  }
5643
5770
  const locals = localComponentNames(tsc, sf);
5644
5771
  const visit = (node) => {
@@ -5741,16 +5868,26 @@ function validateReactPageProps(stack) {
5741
5868
  }
5742
5869
 
5743
5870
  // src/reference-integrity-suite.ts
5871
+ var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
5744
5872
  var REFERENCE_INTEGRITY_RULES = [
5745
5873
  { name: "validateObjectReferences", run: validateObjectReferences },
5746
- { name: "validateSearchableFields", run: validateSearchableFields },
5874
+ // [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
5875
+ // both judge a LIST VIEW's field references, and a standalone list view is
5876
+ // written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
5877
+ // an MCP/AI author has. Their walks read the flattened overlay shape that
5878
+ // door carries (see each rule's `views[]` self rung), and they resolve only
5879
+ // against `stack.objects`, which the per-write snapshot DOES carry — so the
5880
+ // crossing has no missing-collection false-positive channel. Measured over
5881
+ // the shipped view corpus before crossing (0 refusals; population in the
5882
+ // #9313 PR).
5883
+ { name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
5747
5884
  // [#9257] The same reading, one axis over: a list view's `sort` is a field
5748
5885
  // name written in metadata, resolved against the object's declared fields. It
5749
5886
  // gates (`error`) because the runtime does not tolerate a bad one at all —
5750
5887
  // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5751
5888
  // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5752
5889
  // the refusal is the whole view, on every load, traced to nothing.
5753
- { name: "validateSortableFields", run: validateSortableFields },
5890
+ { name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
5754
5891
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5755
5892
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5756
5893
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5856,9 +5993,11 @@ var REFERENCE_INTEGRITY_RULES = [
5856
5993
  // — only a page that is actually `kind:'react'` loads the compiler.
5857
5994
  { name: "validateReactPageProps", run: validateReactPageProps }
5858
5995
  ];
5859
- function validateReferenceIntegrity(stack) {
5996
+ function validateReferenceIntegrity(stack, options) {
5860
5997
  const findings = [];
5998
+ const writeType = options?.runtimeWriteType;
5861
5999
  for (const rule of REFERENCE_INTEGRITY_RULES) {
6000
+ if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
5862
6001
  findings.push(...rule.run(stack));
5863
6002
  }
5864
6003
  return findings;
@@ -9996,9 +10135,31 @@ var AUTHORING_RULES = [
9996
10135
  // collection in the snapshot and return nothing, and the two that would
9997
10136
  // load `typescript` need a hook/action/react body the snapshot never
9998
10137
  // carries — which is what `runtime-lazy-deps.test.ts` pins.
10138
+ //
10139
+ // [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
10140
+ // taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
10141
+ // dispatch the suite, and the suite's own per-member `runtimeTypes`
10142
+ // (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
10143
+ // that snapshot — the gate passes the written type through
10144
+ // `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
10145
+ // members that judge a list view's field references and resolve only
10146
+ // against the `objects` collection the snapshot carries
10147
+ // (`validateSearchableFields`, `validateSortableFields`). The whole suite
10148
+ // is NOT the right granularity for this door, measured not assumed, and
10149
+ // the crossing fails differently per body shape — both ways wrong:
10150
+ // `validateActionNameRefs` (error-tier) resolves `views[].list` /
10151
+ // `views[].listViews.*` action names against `stack.actions`, a collection
10152
+ // no per-write snapshot carries, so on a CONTAINER view write it would
10153
+ // refuse every stack-level action the body names (measured:
10154
+ // `action-name-undefined` on the snapshot shape, clean on the full stack)
10155
+ // — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
10156
+ // type the gate has. On a FLATTENED overlay it has no rung at all, so the
10157
+ // crossing would be a silent no-op that reads as coverage — the very shape
10158
+ // #9313 was filed about. Flow snapshots are unchanged: every member keeps
10159
+ // the default `flow` declaration.
9999
10160
  surfaces: CLI_AND_RUNTIME,
10000
- runtimeTypes: ["flow"],
10001
- run: (stack) => validateReferenceIntegrity(stack)
10161
+ runtimeTypes: ["flow", "view"],
10162
+ run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
10002
10163
  },
10003
10164
  // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
10004
10165
  // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
@@ -10827,6 +10988,20 @@ function buildRuntimeWriteSnapshots(args) {
10827
10988
  };
10828
10989
  return { baseline, candidate };
10829
10990
  }
10991
+ var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
10992
+ var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
10993
+ var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
10994
+ function nameKeyFindingPath(path, candidate) {
10995
+ const m = TOP_LEVEL_INDEX.exec(path);
10996
+ if (!m) return path;
10997
+ const [, stackKey, index, rest] = m;
10998
+ if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
10999
+ const collection = candidate[stackKey];
11000
+ const entry = collection?.[Number(index)];
11001
+ const name = entry && typeof entry === "object" ? entry.name : void 0;
11002
+ if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
11003
+ return `${stackKey}.${name}${rest}`;
11004
+ }
10830
11005
  function runRules(rules, stack, ctx) {
10831
11006
  const findings = [];
10832
11007
  for (const rule of rules) {
@@ -10856,9 +11031,12 @@ function runRuntimeAuthoringRules(args) {
10856
11031
  ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10857
11032
  });
10858
11033
  if (!snapshots) return empty;
10859
- const ctx = { sduiManifest: args.sduiManifest };
11034
+ const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
10860
11035
  const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10861
- const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
11036
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
11037
+ const path = nameKeyFindingPath(f.path, snapshots.candidate);
11038
+ return path === f.path ? f : { ...f, path };
11039
+ });
10862
11040
  return {
10863
11041
  errors: added.filter((f) => f.severity === "error"),
10864
11042
  advisories: added.filter((f) => f.severity !== "error"),