@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.
@@ -1 +1 @@
1
- export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-s3X9D9hm.cjs';
1
+ export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-cV-l_vtC.cjs';
package/dist/runtime.d.ts CHANGED
@@ -1 +1 @@
1
- export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-s3X9D9hm.js';
1
+ export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-cV-l_vtC.js';
package/dist/runtime.js CHANGED
@@ -2185,6 +2185,26 @@ function validateSearchableFields(stack) {
2185
2185
  if (!isRec3(view)) continue;
2186
2186
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
2187
2187
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
2188
+ if (view.viewKind === "list" && !isRec3(view.config)) {
2189
+ check(
2190
+ view.searchableFields,
2191
+ listViewObject(view) ?? viewObject,
2192
+ `view "${viewLabel2}" (flattened list overlay)`,
2193
+ `views[${vi}].searchableFields`,
2194
+ "list-view searchableFields",
2195
+ "narrowing"
2196
+ );
2197
+ }
2198
+ if (view.viewKind === "list" && isRec3(view.config)) {
2199
+ check(
2200
+ view.config.searchableFields,
2201
+ listViewObject(view.config) ?? viewObject,
2202
+ `view "${viewLabel2}" (ViewItem record)`,
2203
+ `views[${vi}].config.searchableFields`,
2204
+ "list-view searchableFields",
2205
+ "narrowing"
2206
+ );
2207
+ }
2188
2208
  if (isRec3(view.list)) {
2189
2209
  check(
2190
2210
  view.list.searchableFields,
@@ -2220,6 +2240,7 @@ function listViewObject(listView) {
2220
2240
  import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
2221
2241
  var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2222
2242
  var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2243
+ var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
2223
2244
  function isRec4(v) {
2224
2245
  return !!v && typeof v === "object" && !Array.isArray(v);
2225
2246
  }
@@ -2282,7 +2303,7 @@ function distance3(a, b) {
2282
2303
  }
2283
2304
  return prev[n];
2284
2305
  }
2285
- function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
2306
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
2286
2307
  const findings = [];
2287
2308
  if (declared === void 0 || declared === null) return findings;
2288
2309
  if (!objectName) return findings;
@@ -2290,10 +2311,23 @@ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path,
2290
2311
  const target = fieldsByObject.get(objectName);
2291
2312
  if (!target) return findings;
2292
2313
  const known = target.names;
2314
+ const anchors = unprovisionedAnchors?.get(objectName);
2293
2315
  for (const key of readSortKeys(declared)) {
2294
2316
  const name = key.field;
2295
2317
  const head = name.split(".")[0];
2296
- if (SYSTEM_FIELDS.has(head)) continue;
2318
+ if (SYSTEM_FIELDS.has(head)) {
2319
+ if (!name.includes(".") && anchors?.has(head)) {
2320
+ findings.push({
2321
+ severity: "warning",
2322
+ rule: SORT_FIELD_UNPROVISIONED,
2323
+ where,
2324
+ path: `${path}${key.at}`,
2325
+ 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.`,
2326
+ hint: unprovisionedAnchorHint(objectName, name)
2327
+ });
2328
+ }
2329
+ continue;
2330
+ }
2297
2331
  if (!known.has(head)) {
2298
2332
  const dotted = name.includes(".");
2299
2333
  findings.push({
@@ -2326,9 +2360,18 @@ function validateSortableFields(stack) {
2326
2360
  if (!isRec4(stack)) return findings;
2327
2361
  const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2328
2362
  const fieldsByObject = indexObjectSearchTargets(stack);
2363
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2329
2364
  const check = (declared, objectName, where, path, subject) => {
2330
2365
  findings.push(
2331
- ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
2366
+ ...checkSortDeclaration(
2367
+ declared,
2368
+ objectName,
2369
+ fieldsByObject,
2370
+ where,
2371
+ path,
2372
+ subject,
2373
+ unprovisionedAnchors
2374
+ )
2332
2375
  );
2333
2376
  };
2334
2377
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2357,6 +2400,24 @@ function validateSortableFields(stack) {
2357
2400
  if (!isRec4(view)) continue;
2358
2401
  const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2359
2402
  const viewObject = strName4(view.objectName) ?? strName4(view.object);
2403
+ if (view.viewKind === "list" && !isRec4(view.config)) {
2404
+ check(
2405
+ view.sort,
2406
+ listViewObject2(view) ?? viewObject,
2407
+ `view "${viewLabel2}" (flattened list overlay)`,
2408
+ `views[${vi}].sort`,
2409
+ "list-view sort"
2410
+ );
2411
+ }
2412
+ if (view.viewKind === "list" && isRec4(view.config)) {
2413
+ check(
2414
+ view.config.sort,
2415
+ listViewObject2(view.config) ?? viewObject,
2416
+ `view "${viewLabel2}" (ViewItem record)`,
2417
+ `views[${vi}].config.sort`,
2418
+ "list-view sort"
2419
+ );
2420
+ }
2360
2421
  if (isRec4(view.list)) {
2361
2422
  check(
2362
2423
  view.list.sort,
@@ -4537,6 +4598,38 @@ function validateAiAgentAuthoring(stack) {
4537
4598
  // src/validate-hook-body-writes.ts
4538
4599
  import { createRequire } from "module";
4539
4600
  import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
4601
+
4602
+ // src/checked-parse.ts
4603
+ function createSourceFileChecked(tsc, fileName, source, options) {
4604
+ const sourceFile = tsc.createSourceFile(
4605
+ fileName,
4606
+ source,
4607
+ options.target,
4608
+ options.setParentNodes,
4609
+ options.scriptKind
4610
+ );
4611
+ const diagnostics = sourceFile.parseDiagnostics;
4612
+ if (!diagnostics || diagnostics.length === 0) return { sourceFile };
4613
+ const first = diagnostics[0];
4614
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
4615
+ const offset = options.synthesizedLinesBefore ?? 0;
4616
+ return {
4617
+ sourceFile,
4618
+ failure: {
4619
+ message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
4620
+ line: Math.max(1, line + 1 - offset),
4621
+ column: character + 1,
4622
+ count: diagnostics.length
4623
+ }
4624
+ };
4625
+ }
4626
+ function describeParseFailure(failure) {
4627
+ const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
4628
+ return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
4629
+ }
4630
+ 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.";
4631
+
4632
+ // src/validate-hook-body-writes.ts
4540
4633
  var cachedTs = null;
4541
4634
  function loadTypeScript() {
4542
4635
  if (cachedTs) return cachedTs;
@@ -4551,6 +4644,7 @@ function loadTypeScript() {
4551
4644
  return cachedTs;
4552
4645
  }
4553
4646
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4647
+ var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
4554
4648
  var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4555
4649
  var HOOK_BODY_WRITE_PATTERNS = [
4556
4650
  {
@@ -4650,23 +4744,23 @@ function judgeableFieldsOf(index, objectName) {
4650
4744
  if (!declared || declared.size === 0) return void 0;
4651
4745
  return declared;
4652
4746
  }
4653
- function extractHookBodyWrites(source) {
4654
- return extractHookBodyWriteSet(source).writes;
4655
- }
4656
4747
  function extractHookBodyWriteSet(source) {
4657
4748
  if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
4658
4749
  return { writes: [], ctxRecordEscapes: false };
4659
4750
  }
4660
4751
  const tsc = loadTypeScript();
4661
- const sf = tsc.createSourceFile(
4752
+ const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
4753
+ tsc,
4662
4754
  "hook-body.ts",
4663
4755
  `async function __body(ctx) {
4664
4756
  ${source}
4665
4757
  }`,
4666
- tsc.ScriptTarget.Latest,
4667
- /* setParentNodes */
4668
- false,
4669
- tsc.ScriptKind.TS
4758
+ {
4759
+ target: tsc.ScriptTarget.Latest,
4760
+ setParentNodes: false,
4761
+ scriptKind: tsc.ScriptKind.TS,
4762
+ synthesizedLinesBefore: 1
4763
+ }
4670
4764
  );
4671
4765
  const writes = [];
4672
4766
  const recordRefs = [];
@@ -4763,7 +4857,8 @@ ${source}
4763
4857
  visit(sf);
4764
4858
  return {
4765
4859
  writes,
4766
- ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
4860
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
4861
+ ...parseFailure ? { parseFailure } : {}
4767
4862
  };
4768
4863
  }
4769
4864
  function validateHookBodyWrites(stack) {
@@ -4777,11 +4872,22 @@ function validateHookBodyWrites(stack) {
4777
4872
  if (!isRec15(body) || body.language !== "js") return;
4778
4873
  const source = body.source;
4779
4874
  if (typeof source !== "string" || source.trim() === "") return;
4780
- const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4875
+ const extracted = extractHookBodyWriteSet(source);
4876
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4877
+ if (extracted.parseFailure) {
4878
+ findings.push({
4879
+ severity: "warning",
4880
+ rule: HOOK_BODY_SOURCE_UNPARSEABLE,
4881
+ where: `hook "${hookName}" \u203A body`,
4882
+ path: `hooks[${hookIndex}].body.source`,
4883
+ 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.`,
4884
+ hint: PARSE_FAILURE_HINT
4885
+ });
4886
+ }
4887
+ const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4781
4888
  if (writes.length === 0) return;
4782
4889
  objectFields ?? (objectFields = indexObjectFields2(stack));
4783
4890
  anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4784
- const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4785
4891
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4786
4892
  (o) => typeof o === "string" && o.trim() !== ""
4787
4893
  );
@@ -4866,6 +4972,7 @@ function fixHint(field, declared) {
4866
4972
  import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
4867
4973
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4868
4974
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4975
+ var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
4869
4976
  var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4870
4977
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4871
4978
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
@@ -4923,11 +5030,21 @@ function validateActionBodyWrites(stack) {
4923
5030
  let anchors = null;
4924
5031
  for (const site of sites) {
4925
5032
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4926
- const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5033
+ const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
4927
5034
  const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
4928
5035
  const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
4929
- if (writes.length === 0 && recordWrites.length === 0) continue;
4930
5036
  const where = `action "${site.name}" \u203A body`;
5037
+ if (parseFailure) {
5038
+ findings.push({
5039
+ severity: "warning",
5040
+ rule: ACTION_BODY_SOURCE_UNPARSEABLE,
5041
+ where,
5042
+ path: site.path,
5043
+ 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.`,
5044
+ hint: PARSE_FAILURE_HINT
5045
+ });
5046
+ }
5047
+ if (writes.length === 0 && recordWrites.length === 0) continue;
4931
5048
  if (recordWrites.length > 0 && !ctxRecordEscapes) {
4932
5049
  const reportedFields = /* @__PURE__ */ new Set();
4933
5050
  for (const w of recordWrites) {
@@ -5307,6 +5424,7 @@ function filterAttrValue(tsc, sf, attr) {
5307
5424
  };
5308
5425
  return perPosition(init.expression);
5309
5426
  }
5427
+ var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
5310
5428
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
5311
5429
  var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
5312
5430
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
@@ -5635,11 +5753,20 @@ function validateReactPageProps(stack) {
5635
5753
  if (typeof source !== "string" || source.trim() === "") continue;
5636
5754
  const name = String(page.name ?? `#${p}`);
5637
5755
  const tsc = loadTypeScript2();
5638
- let sf;
5639
- try {
5640
- sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
5641
- } catch {
5642
- continue;
5756
+ const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
5757
+ target: tsc.ScriptTarget.Latest,
5758
+ setParentNodes: true,
5759
+ scriptKind: tsc.ScriptKind.TSX
5760
+ });
5761
+ if (failure) {
5762
+ findings.push({
5763
+ severity: "warning",
5764
+ rule: REACT_PAGE_SOURCE_UNPARSEABLE,
5765
+ where: `page "${name}"`,
5766
+ path: `pages[${p}].source`,
5767
+ 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.`,
5768
+ hint: PARSE_FAILURE_HINT
5769
+ });
5643
5770
  }
5644
5771
  const locals = localComponentNames(tsc, sf);
5645
5772
  const visit = (node) => {
@@ -5742,16 +5869,26 @@ function validateReactPageProps(stack) {
5742
5869
  }
5743
5870
 
5744
5871
  // src/reference-integrity-suite.ts
5872
+ var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
5745
5873
  var REFERENCE_INTEGRITY_RULES = [
5746
5874
  { name: "validateObjectReferences", run: validateObjectReferences },
5747
- { name: "validateSearchableFields", run: validateSearchableFields },
5875
+ // [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
5876
+ // both judge a LIST VIEW's field references, and a standalone list view is
5877
+ // written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
5878
+ // an MCP/AI author has. Their walks read the flattened overlay shape that
5879
+ // door carries (see each rule's `views[]` self rung), and they resolve only
5880
+ // against `stack.objects`, which the per-write snapshot DOES carry — so the
5881
+ // crossing has no missing-collection false-positive channel. Measured over
5882
+ // the shipped view corpus before crossing (0 refusals; population in the
5883
+ // #9313 PR).
5884
+ { name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
5748
5885
  // [#9257] The same reading, one axis over: a list view's `sort` is a field
5749
5886
  // name written in metadata, resolved against the object's declared fields. It
5750
5887
  // gates (`error`) because the runtime does not tolerate a bad one at all —
5751
5888
  // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5752
5889
  // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5753
5890
  // the refusal is the whole view, on every load, traced to nothing.
5754
- { name: "validateSortableFields", run: validateSortableFields },
5891
+ { name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
5755
5892
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5756
5893
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5757
5894
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5857,9 +5994,11 @@ var REFERENCE_INTEGRITY_RULES = [
5857
5994
  // — only a page that is actually `kind:'react'` loads the compiler.
5858
5995
  { name: "validateReactPageProps", run: validateReactPageProps }
5859
5996
  ];
5860
- function validateReferenceIntegrity(stack) {
5997
+ function validateReferenceIntegrity(stack, options) {
5861
5998
  const findings = [];
5999
+ const writeType = options?.runtimeWriteType;
5862
6000
  for (const rule of REFERENCE_INTEGRITY_RULES) {
6001
+ if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
5863
6002
  findings.push(...rule.run(stack));
5864
6003
  }
5865
6004
  return findings;
@@ -10016,9 +10155,31 @@ var AUTHORING_RULES = [
10016
10155
  // collection in the snapshot and return nothing, and the two that would
10017
10156
  // load `typescript` need a hook/action/react body the snapshot never
10018
10157
  // carries — which is what `runtime-lazy-deps.test.ts` pins.
10158
+ //
10159
+ // [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
10160
+ // taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
10161
+ // dispatch the suite, and the suite's own per-member `runtimeTypes`
10162
+ // (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
10163
+ // that snapshot — the gate passes the written type through
10164
+ // `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
10165
+ // members that judge a list view's field references and resolve only
10166
+ // against the `objects` collection the snapshot carries
10167
+ // (`validateSearchableFields`, `validateSortableFields`). The whole suite
10168
+ // is NOT the right granularity for this door, measured not assumed, and
10169
+ // the crossing fails differently per body shape — both ways wrong:
10170
+ // `validateActionNameRefs` (error-tier) resolves `views[].list` /
10171
+ // `views[].listViews.*` action names against `stack.actions`, a collection
10172
+ // no per-write snapshot carries, so on a CONTAINER view write it would
10173
+ // refuse every stack-level action the body names (measured:
10174
+ // `action-name-undefined` on the snapshot shape, clean on the full stack)
10175
+ // — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
10176
+ // type the gate has. On a FLATTENED overlay it has no rung at all, so the
10177
+ // crossing would be a silent no-op that reads as coverage — the very shape
10178
+ // #9313 was filed about. Flow snapshots are unchanged: every member keeps
10179
+ // the default `flow` declaration.
10019
10180
  surfaces: CLI_AND_RUNTIME,
10020
- runtimeTypes: ["flow"],
10021
- run: (stack) => validateReferenceIntegrity(stack)
10181
+ runtimeTypes: ["flow", "view"],
10182
+ run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
10022
10183
  },
10023
10184
  // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
10024
10185
  // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
@@ -10847,6 +11008,20 @@ function buildRuntimeWriteSnapshots(args) {
10847
11008
  };
10848
11009
  return { baseline, candidate };
10849
11010
  }
11011
+ var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
11012
+ var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
11013
+ var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
11014
+ function nameKeyFindingPath(path, candidate) {
11015
+ const m = TOP_LEVEL_INDEX.exec(path);
11016
+ if (!m) return path;
11017
+ const [, stackKey, index, rest] = m;
11018
+ if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
11019
+ const collection = candidate[stackKey];
11020
+ const entry = collection?.[Number(index)];
11021
+ const name = entry && typeof entry === "object" ? entry.name : void 0;
11022
+ if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
11023
+ return `${stackKey}.${name}${rest}`;
11024
+ }
10850
11025
  function runRules(rules, stack, ctx) {
10851
11026
  const findings = [];
10852
11027
  for (const rule of rules) {
@@ -10876,9 +11051,12 @@ function runRuntimeAuthoringRules(args) {
10876
11051
  ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10877
11052
  });
10878
11053
  if (!snapshots) return empty;
10879
- const ctx = { sduiManifest: args.sduiManifest };
11054
+ const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
10880
11055
  const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10881
- const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
11056
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
11057
+ const path = nameKeyFindingPath(f.path, snapshots.candidate);
11058
+ return path === f.path ? f : { ...f, path };
11059
+ });
10882
11060
  return {
10883
11061
  errors: added.filter((f) => f.severity === "error"),
10884
11062
  advisories: added.filter((f) => f.severity !== "error"),