@habitat-ai/service 0.2.0 → 0.2.2

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,5 +1,5 @@
1
1
  import { Validator } from "typebox/schema";
2
- import { BlueprintDefinitionSchema, CompatibilityIndexSchema, CompatibilityRuleSourceSchema, HabitatInstanceManifestSchema, MAX_CATALOG_ISSUES, PolicyPackManifestSchema, PolicyPackPackageJsonSchema, } from "../dto/catalog.js";
2
+ import { BlueprintDefinitionSchema, CompatibilityBaselineSchema, CompatibilityIndexSchema, CompatibilityRuleSourceSchema, HabitatInstanceManifestSchema, MAX_CATALOG_ISSUES, PolicyPackManifestSchema, PolicyPackPackageJsonSchema, } from "../dto/catalog.js";
3
3
  const MEMBER_PLACEHOLDER = "{member}";
4
4
  const GLOB_CHARACTERS = /[*?[\]!]/;
5
5
  const blueprintValidator = new Validator({}, BlueprintDefinitionSchema);
@@ -8,6 +8,7 @@ const policyPackPackageJsonValidator = new Validator({}, PolicyPackPackageJsonSc
8
8
  const instanceValidator = new Validator({}, HabitatInstanceManifestSchema);
9
9
  const compatibilityIndexValidator = new Validator({}, CompatibilityIndexSchema);
10
10
  const compatibilityRuleValidator = new Validator({}, CompatibilityRuleSourceSchema);
11
+ const compatibilityBaselineValidator = new Validator({}, CompatibilityBaselineSchema);
11
12
  /** Validates the selected policy-pack filesystem locators. */
12
13
  export function admitPolicyPackSelection(selection, path) {
13
14
  const issues = [];
@@ -73,9 +74,31 @@ export function admitCompatibilityRule(value, relativePath) {
73
74
  const admitted = admit(compatibilityRuleValidator, value, relativePath);
74
75
  return admitted.ok ? { ok: true, source: { rule: admitted.value, relativePath } } : admitted;
75
76
  }
77
+ /** Admits the exact empty baseline supported by compatibility execution. */
78
+ export function admitCompatibilityBaseline(value, relativePath) {
79
+ return admit(compatibilityBaselineValidator, value, relativePath);
80
+ }
76
81
  /** Collects filesystem paths that schema-admitted documents require the service to observe. */
77
82
  export function referencedRepositoryPaths(documents, path) {
78
83
  const references = new Set();
84
+ for (const root of Object.values(documents.compatibilityIndex?.ownerRoots ?? {})) {
85
+ if (relativePathIssues(root, ".habitat/index.json", path).length === 0) {
86
+ references.add(toRepositoryPath(root, path));
87
+ }
88
+ }
89
+ for (const source of documents.compatibilityRules) {
90
+ const rule = source.rule;
91
+ const paths = [
92
+ rule.supportFiles.baseline,
93
+ rule.runner.name === "grit" ? rule.runner.files.pattern : rule.runner.files.structure,
94
+ ...(rule.runner.name === "grit" ? rule.runner.acquisition.roots : []),
95
+ ];
96
+ for (const referencedPath of paths) {
97
+ if (relativePathIssues(referencedPath, source.relativePath, path).length === 0) {
98
+ references.add(toRepositoryPath(referencedPath, path));
99
+ }
100
+ }
101
+ }
79
102
  for (const source of documents.blueprints) {
80
103
  const directory = path.dirname(source.relativePath);
81
104
  for (const rule of source.definition.rules) {
@@ -126,10 +149,10 @@ export function resolveCatalog(documents, pathFacts, workspaceRoot, workspaceRea
126
149
  for (const source of blueprints) {
127
150
  issues.push(...validateBlueprint(source, path));
128
151
  }
129
- const compatibility = resolveCompatibility(documents, issues, path);
152
+ const compatibility = resolveCompatibility(documents, issues, pathFacts, workspaceRoot, workspaceRealRoot, path);
130
153
  const ruleSources = [
131
154
  ...blueprints.flatMap((source) => source.definition.rules.map((rule) => ({ identity: rule.id, path: source.relativePath }))),
132
- ...compatibility.rules.map((rule) => ({ identity: rule.id, path: rule.manifestPath })),
155
+ ...compatibility.rules.map((rule) => ({ identity: rule.ruleId, path: rule.manifestPath })),
133
156
  ];
134
157
  issues.push(...duplicateIssues(ruleSources, "authority-duplicate-rule", "rule"));
135
158
  const definitions = new Map(blueprints.map((source) => [blueprintIdentity(source.definition), source]));
@@ -276,24 +299,74 @@ export function rejected(issues) {
276
299
  : [issue("authority-resolution-failed", "", "Catalog resolution failed.")],
277
300
  };
278
301
  }
279
- function resolveCompatibility(documents, issues, path) {
302
+ function resolveCompatibility(documents, issues, pathFacts, workspaceRoot, workspaceRealRoot, path) {
280
303
  if (!documents.compatibilityIndex) {
281
304
  return { schemaVersion: 2, ownerRoots: {}, rules: [] };
282
305
  }
283
306
  const ownerRoots = Object.fromEntries(Object.entries(documents.compatibilityIndex.ownerRoots).sort(([left], [right]) => textOrder(left, right)));
284
307
  for (const [owner, root] of Object.entries(ownerRoots)) {
285
308
  issues.push(...relativePathIssues(root, `.habitat/index.json#ownerRoots:${owner}`, path));
309
+ issues.push(...pathFactIssues(root, "directory", `.habitat/index.json#ownerRoots:${owner}`, pathFacts, workspaceRoot, workspaceRealRoot, path));
286
310
  }
287
311
  const rules = [...documents.compatibilityRules]
288
312
  .sort((left, right) => textOrder(left.rule.id, right.rule.id) || textOrder(left.relativePath, right.relativePath))
289
313
  .map((source) => {
290
- if (!Object.hasOwn(ownerRoots, source.rule.ownerProject)) {
291
- issues.push(issue("authority-compatibility-invalid", source.relativePath, `Legacy rule ownerProject "${source.rule.ownerProject}" has no owner root.`));
314
+ const rule = source.rule;
315
+ if (!Object.hasOwn(ownerRoots, rule.ownerProject)) {
316
+ issues.push(issue("authority-compatibility-invalid", source.relativePath, `Legacy rule ownerProject "${rule.ownerProject}" has no owner root.`));
292
317
  }
293
- return {
294
- id: source.rule.id,
295
- ownerProject: source.rule.ownerProject,
318
+ for (const pattern of rule.pathCoverage[0].patterns) {
319
+ issues.push(...repositoryPatternIssues(pattern, source.relativePath, path));
320
+ }
321
+ const baselinePath = rule.supportFiles.baseline;
322
+ const runnerAssetPath = rule.runner.name === "grit" ? rule.runner.files.pattern : rule.runner.files.structure;
323
+ issues.push(...pathFactIssues(baselinePath, "file", source.relativePath, pathFacts, workspaceRoot, workspaceRealRoot, path), ...pathFactIssues(runnerAssetPath, "file", source.relativePath, pathFacts, workspaceRoot, workspaceRealRoot, path));
324
+ const provenance = {
325
+ kind: "local",
326
+ authorityRoot: workspaceRoot,
327
+ relativePath: source.relativePath,
328
+ };
329
+ const asset = (relativePath) => ({
330
+ provenance,
331
+ relativePath,
332
+ absolutePath: path.resolve(workspaceRoot, relativePath),
333
+ });
334
+ const common = {
335
+ ruleId: rule.id,
336
+ ownerProject: rule.ownerProject,
296
337
  manifestPath: source.relativePath,
338
+ lane: rule.lane,
339
+ message: rule.message,
340
+ remediate: rule.remediate,
341
+ provenance,
342
+ coveragePatterns: [...rule.pathCoverage[0].patterns],
343
+ baseline: asset(baselinePath),
344
+ };
345
+ if (rule.runner.name === "habitat") {
346
+ return {
347
+ ...common,
348
+ runner: {
349
+ name: "habitat",
350
+ mode: "structure",
351
+ structure: asset(runnerAssetPath),
352
+ },
353
+ };
354
+ }
355
+ const entries = [];
356
+ for (const root of rule.runner.acquisition.roots) {
357
+ const admission = pathFactAnyIssues(root, source.relativePath, pathFacts, workspaceRoot, workspaceRealRoot, path);
358
+ issues.push(...admission.issues);
359
+ if (admission.kind !== undefined)
360
+ entries.push({ kind: admission.kind, path: root });
361
+ }
362
+ return {
363
+ ...common,
364
+ runner: {
365
+ name: "grit",
366
+ pattern: asset(runnerAssetPath),
367
+ patternName: rule.runner.patternName,
368
+ acquisition: { kind: "check", entries },
369
+ },
297
370
  };
298
371
  });
299
372
  return { schemaVersion: 2, ownerRoots, rules };
@@ -501,6 +574,15 @@ function pathFactIssues(relativePath, expectedKind, sourcePath, facts, workspace
501
574
  }
502
575
  return [];
503
576
  }
577
+ function pathFactAnyIssues(relativePath, sourcePath, facts, workspaceRoot, workspaceRealRoot, path) {
578
+ const normalized = toRepositoryPath(relativePath, path);
579
+ const fact = facts.get(normalized);
580
+ const kind = fact?.kind === "directory" || fact?.kind === "file" ? fact.kind : undefined;
581
+ return {
582
+ kind,
583
+ issues: pathFactIssues(normalized, kind ?? "directory", sourcePath, facts, workspaceRoot, workspaceRealRoot, path),
584
+ };
585
+ }
504
586
  function ruleAssetPath(source, rule, path) {
505
587
  const asset = rule.runner.name === "habitat" ? rule.runner.structure : rule.runner.pattern;
506
588
  return toRepositoryPath(path.join(path.dirname(source.relativePath), asset), path);
@@ -520,6 +602,19 @@ function relativePathIssues(candidate, sourcePath, path) {
520
602
  ]
521
603
  : [];
522
604
  }
605
+ function repositoryPatternIssues(candidate, sourcePath, path) {
606
+ const invalid = candidate.includes("\\") ||
607
+ path.isAbsolute(candidate) ||
608
+ candidate.includes("//") ||
609
+ candidate.endsWith("/") ||
610
+ candidate.split("/").some((segment) => segment === "" || segment === "..") ||
611
+ /[\u0000-\u001f\u007f]/u.test(candidate);
612
+ return invalid
613
+ ? [
614
+ issue("authority-path-invalid", sourcePath, `Coverage pattern must be repository-relative and traversal-free: "${candidate}".`),
615
+ ]
616
+ : [];
617
+ }
523
618
  function pathTemplateIssues(template, sourcePath, path) {
524
619
  const placeholderCount = template.split(MEMBER_PLACEHOLDER).length - 1;
525
620
  const substituted = template.replaceAll(MEMBER_PLACEHOLDER, "member");
@@ -2,7 +2,9 @@ import type { RuleEvaluationFinding } from "@habitat-ai/resource-rule-evaluation
2
2
  import type { HabitatCatalog } from "../dto/catalog.js";
3
3
  import type { CheckApplicationReport, CheckCatalogInput, CheckCatalogResult, CheckSelectionIssue } from "../dto/check.js";
4
4
  import type { HabitatStructureApplication, StructureDiagnostic } from "./structure.js";
5
- type RuleApplication = HabitatCatalog["applications"][number];
5
+ type InstanceApplication = HabitatCatalog["applications"][number];
6
+ type CompatibilityRule = HabitatCatalog["compatibility"]["rules"][number];
7
+ type RuleApplication = InstanceApplication | CompatibilityRule;
6
8
  type ResolvedGritRunner = Extract<RuleApplication["runner"], {
7
9
  name: "grit";
8
10
  }>;
@@ -12,7 +14,7 @@ type GritCheckRunner = Omit<ResolvedGritRunner, "acquisition"> & {
12
14
  };
13
15
  };
14
16
  /** Resolved application mechanically executable by the Grit check resource. */
15
- export type GritCheckApplication = Omit<RuleApplication, "runner"> & {
17
+ export type GritCheckApplication = RuleApplication & {
16
18
  readonly runner: GritCheckRunner;
17
19
  };
18
20
  /** Runner set executable by catalog.check. */
@@ -23,6 +25,12 @@ type GritCheckApplicationReport = Extract<CheckApplicationReport, {
23
25
  type StructureCheckApplicationReport = Extract<CheckApplicationReport, {
24
26
  runner: "habitat";
25
27
  }>;
28
+ type CompatibilityGritCheckApplicationReport = Extract<GritCheckApplicationReport, {
29
+ instanceId: null;
30
+ }>;
31
+ type CompatibilityGritCheckApplication = Extract<GritCheckApplication, {
32
+ readonly baseline: unknown;
33
+ }>;
26
34
  type CheckSelection = {
27
35
  readonly ok: true;
28
36
  readonly applications: readonly ExecutableCheckApplication[];
@@ -46,6 +54,8 @@ export declare function evaluatedApplication(application: GritCheckApplication,
46
54
  export declare function failedApplication(application: GritCheckApplication, reason: Extract<GritCheckApplicationReport["disposition"], {
47
55
  kind: "failed";
48
56
  }>["reason"], detail: string): GritCheckApplicationReport;
57
+ /** Reports an exact-coverage compatibility rule that has no current subject files. */
58
+ export declare function notApplicableApplication(application: CompatibilityGritCheckApplication): CompatibilityGritCheckApplicationReport;
49
59
  /** Produces one native Habitat report from pure structure diagnostics. */
50
60
  export declare function evaluatedStructureApplication(application: HabitatStructureApplication, diagnostics: readonly StructureDiagnostic[]): StructureCheckApplicationReport;
51
61
  /** Produces one deterministic native Habitat operational-error report. */
@@ -54,4 +64,6 @@ export declare function failedStructureApplication(application: HabitatStructure
54
64
  }>["reason"], detail: string): StructureCheckApplicationReport;
55
65
  /** Completes a check from already ordered application reports. */
56
66
  export declare function completedCheck(applications: readonly CheckApplicationReport[]): CheckCatalogResult;
67
+ /** Narrows a resolved check application to one compatibility rule. */
68
+ export declare function isCompatibilityRule(application: RuleApplication): application is CompatibilityRule;
57
69
  export {};
@@ -36,8 +36,13 @@ export function selectCheckApplications(catalog, input) {
36
36
  }
37
37
  if (issues.length > 0)
38
38
  return refused(issues);
39
- const selected = catalog.applications.filter((application) => (selectors?.owner === undefined || application.ownerProject === selectors.owner) &&
40
- (selectors?.instance === undefined || application.instanceId === selectors.instance) &&
39
+ const applications = [
40
+ ...catalog.applications,
41
+ ...catalog.compatibility.rules,
42
+ ];
43
+ const selected = applications.filter((application) => (selectors?.owner === undefined || application.ownerProject === selectors.owner) &&
44
+ (selectors?.instance === undefined ||
45
+ (isInstanceApplication(application) && application.instanceId === selectors.instance)) &&
41
46
  (requestedRules.length === 0 || requestedRules.includes(application.ruleId)) &&
42
47
  (selectors?.runner === undefined || application.runner.name === selectors.runner));
43
48
  if (hasExplicitSelectors && selected.length === 0 && !isKnownEmptyRunnerSelection(selectors)) {
@@ -47,7 +52,7 @@ export function selectCheckApplications(catalog, input) {
47
52
  }
48
53
  const unsupported = selected.filter(isUnsupportedGritApplication);
49
54
  if (unsupported.length > 0) {
50
- return refused(unsupported.map((application) => issue("runner-unsupported", `${application.instanceId}:${application.ruleId}`, `Rule "${application.ruleId}" requires Grit ${application.runner.acquisition.kind}, which catalog.check does not execute.`)));
55
+ return refused(unsupported.map((application) => issue("runner-unsupported", applicationIdentityText(application), `Rule "${application.ruleId}" requires Grit ${application.runner.acquisition.kind}, which catalog.check does not execute.`)));
51
56
  }
52
57
  return {
53
58
  ok: true,
@@ -97,6 +102,22 @@ export function failedApplication(application, reason, detail) {
97
102
  findings: [],
98
103
  };
99
104
  }
105
+ /** Reports an exact-coverage compatibility rule that has no current subject files. */
106
+ export function notApplicableApplication(application) {
107
+ return {
108
+ ownerProject: application.ownerProject,
109
+ instanceId: null,
110
+ ruleId: application.ruleId,
111
+ runner: "grit",
112
+ lane: application.lane,
113
+ locked: true,
114
+ message: application.message,
115
+ remediate: application.remediate,
116
+ status: "pass",
117
+ disposition: { kind: "not-applicable", reason: "no-matched-acquisition-roots" },
118
+ findings: [],
119
+ };
120
+ }
100
121
  /** Produces one native Habitat report from pure structure diagnostics. */
101
122
  export function evaluatedStructureApplication(application, diagnostics) {
102
123
  const severity = application.lane === "enforced" ? "error" : "advisory";
@@ -133,6 +154,18 @@ export function completedCheck(applications) {
133
154
  };
134
155
  }
135
156
  function applicationIdentity(application) {
157
+ if (isCompatibilityRule(application)) {
158
+ return {
159
+ ownerProject: application.ownerProject,
160
+ instanceId: null,
161
+ ruleId: application.ruleId,
162
+ runner: "grit",
163
+ lane: application.lane,
164
+ locked: true,
165
+ message: application.message,
166
+ remediate: application.remediate,
167
+ };
168
+ }
136
169
  return {
137
170
  ownerProject: application.ownerProject,
138
171
  instanceId: application.instanceId,
@@ -145,6 +178,18 @@ function applicationIdentity(application) {
145
178
  };
146
179
  }
147
180
  function structureApplicationIdentity(application) {
181
+ if (isCompatibilityRule(application)) {
182
+ return {
183
+ ownerProject: application.ownerProject,
184
+ instanceId: null,
185
+ ruleId: application.ruleId,
186
+ runner: "habitat",
187
+ lane: application.lane,
188
+ locked: true,
189
+ message: application.message,
190
+ remediate: application.remediate,
191
+ };
192
+ }
148
193
  return {
149
194
  ownerProject: application.ownerProject,
150
195
  instanceId: application.instanceId,
@@ -167,7 +212,7 @@ function isExecutableCheckApplication(application) {
167
212
  }
168
213
  function compareApplications(left, right) {
169
214
  return (compareText(left.ruleId, right.ruleId) ||
170
- compareText(left.instanceId, right.instanceId) ||
215
+ compareText(instanceId(left) ?? "", instanceId(right) ?? "") ||
171
216
  compareText(left.ownerProject, right.ownerProject));
172
217
  }
173
218
  function compareText(left, right) {
@@ -180,11 +225,15 @@ function issue(code, selector, message) {
180
225
  return { code, selector, message };
181
226
  }
182
227
  function selectionIssue(catalog, kind, value) {
228
+ const applications = [
229
+ ...catalog.applications,
230
+ ...catalog.compatibility.rules,
231
+ ];
183
232
  const matches = {
184
- owner: catalog.applications.some((application) => application.ownerProject === value),
233
+ owner: applications.some((application) => application.ownerProject === value),
185
234
  instance: catalog.applications.some((application) => application.instanceId === value),
186
- rule: catalog.applications.some((application) => application.ruleId === value),
187
- runner: catalog.applications.some((application) => application.runner.name === value),
235
+ rule: applications.some((application) => application.ruleId === value),
236
+ runner: applications.some((application) => application.runner.name === value),
188
237
  };
189
238
  if (matches[kind] || (kind === "runner" && knownRunnerSelectors.has(value)))
190
239
  return undefined;
@@ -193,6 +242,20 @@ function selectionIssue(catalog, kind, value) {
193
242
  ? issue("selector-unknown", `${kind}:${value}`, `No resolved application uses ${kind} "${value}".`)
194
243
  : issue("selector-wrong-namespace", `${kind}:${value}`, `"${value}" is a known ${actualKind}, not a ${kind}.`);
195
244
  }
245
+ /** Narrows a resolved check application to one compatibility rule. */
246
+ export function isCompatibilityRule(application) {
247
+ return "baseline" in application;
248
+ }
249
+ function isInstanceApplication(application) {
250
+ return "instanceId" in application;
251
+ }
252
+ function instanceId(application) {
253
+ return isInstanceApplication(application) ? application.instanceId : null;
254
+ }
255
+ function applicationIdentityText(application) {
256
+ const instance = instanceId(application);
257
+ return instance === null ? application.ruleId : `${instance}:${application.ruleId}`;
258
+ }
196
259
  function isKnownEmptyRunnerSelection(selectors) {
197
260
  return (selectors?.owner === undefined &&
198
261
  selectors?.instance === undefined &&
@@ -1,8 +1,7 @@
1
1
  import type { SourceInventoryResult } from "@habitat-ai/resource-source-inventory";
2
2
  import type { HabitatCatalog } from "../dto/catalog.js";
3
3
  import type { StructureCheckFinding } from "../dto/check.js";
4
- import { type StructureScope } from "../dto/structure.js";
5
- type RuleApplication = HabitatCatalog["applications"][number];
4
+ type RuleApplication = HabitatCatalog["applications"][number] | HabitatCatalog["compatibility"]["rules"][number];
6
5
  /** Resolved application executable by the native Habitat structure policy. */
7
6
  export type HabitatStructureApplication = RuleApplication & {
8
7
  readonly runner: Extract<RuleApplication["runner"], {
@@ -12,9 +11,16 @@ export type HabitatStructureApplication = RuleApplication & {
12
11
  /** Narrows one resolved application to the native structure runner. */
13
12
  export declare function isHabitatStructureApplication(application: RuleApplication): application is HabitatStructureApplication;
14
13
  /** Live or inventory-derived kind understood by native structure evaluation. */
15
- export type StructureRootKind = StructureScope["kind"] | "other";
16
- type BoundStructureScope = Omit<StructureScope, "allowEmpty"> & {
14
+ export type StructureRootKind = "directory" | "file" | "other";
15
+ type BoundStructureScope = {
16
+ readonly name: string;
17
+ readonly relativePath: string;
18
+ readonly kind: "directory" | "file";
19
+ readonly mode: "open" | "closed";
17
20
  readonly allowEmpty: boolean;
21
+ readonly required?: readonly string[];
22
+ readonly allowed?: readonly string[];
23
+ readonly forbidden?: readonly string[];
18
24
  readonly bindingPath: string;
19
25
  };
20
26
  /** Schema-admitted structure application with inapplicable unbound scopes removed. */
@@ -1,13 +1,43 @@
1
1
  import picomatch from "picomatch";
2
2
  import { Validator } from "typebox/schema";
3
- import { STRUCTURE_PICOMATCH_OPTIONS, StructureDocumentSchema, } from "../dto/structure.js";
3
+ import { CompatibilityStructureDocumentSchema, STRUCTURE_PICOMATCH_OPTIONS, StructureDocumentSchema, } from "../dto/structure.js";
4
4
  /** Narrows one resolved application to the native structure runner. */
5
5
  export function isHabitatStructureApplication(application) {
6
6
  return application.runner.name === "habitat";
7
7
  }
8
8
  const structureValidator = new Validator({}, StructureDocumentSchema);
9
+ const compatibilityStructureValidator = new Validator({}, CompatibilityStructureDocumentSchema);
9
10
  /** Admits TypeBox-valid structure authority and resolves its root-role applicability. */
10
11
  export function admitStructureDocument(value, application) {
12
+ if (isCompatibilityStructureApplication(application)) {
13
+ if (!compatibilityStructureValidator.Check(value)) {
14
+ const [, errors] = compatibilityStructureValidator.Errors(value);
15
+ return {
16
+ ok: false,
17
+ detail: errors
18
+ .slice(0, 20)
19
+ .map((error) => error.message)
20
+ .join("; ") || "Structure document does not satisfy compatibility schema version 1.",
21
+ };
22
+ }
23
+ return {
24
+ ok: true,
25
+ admitted: {
26
+ application,
27
+ scopes: value.scopes.map((scope) => ({
28
+ name: scope.name,
29
+ relativePath: scope.root,
30
+ kind: scope.kind,
31
+ mode: scope.mode,
32
+ allowEmpty: scope.allowEmpty ?? false,
33
+ ...(scope.required === undefined ? {} : { required: scope.required }),
34
+ ...(scope.allowed === undefined ? {} : { allowed: scope.allowed }),
35
+ ...(scope.forbidden === undefined ? {} : { forbidden: scope.forbidden }),
36
+ bindingPath: ".",
37
+ })),
38
+ },
39
+ };
40
+ }
11
41
  if (!structureValidator.Check(value)) {
12
42
  const [, errors] = structureValidator.Errors(value);
13
43
  return {
@@ -40,10 +70,23 @@ export function admitStructureDocument(value, application) {
40
70
  detail: `Structure scope "${scope.name}" resolves beyond the maximum repository path length.`,
41
71
  };
42
72
  }
43
- scopes.push({ ...scope, allowEmpty: scope.allowEmpty ?? false, bindingPath: binding.path });
73
+ scopes.push({
74
+ name: scope.name,
75
+ relativePath: scope.relativePath,
76
+ kind: scope.kind,
77
+ mode: scope.mode,
78
+ allowEmpty: scope.allowEmpty ?? false,
79
+ ...(scope.required === undefined ? {} : { required: scope.required }),
80
+ ...(scope.allowed === undefined ? {} : { allowed: scope.allowed }),
81
+ ...(scope.forbidden === undefined ? {} : { forbidden: scope.forbidden }),
82
+ bindingPath: binding.path,
83
+ });
44
84
  }
45
85
  return { ok: true, admitted: { application, scopes } };
46
86
  }
87
+ function isCompatibilityStructureApplication(application) {
88
+ return "baseline" in application;
89
+ }
47
90
  /** Derives ancestors and direct children while pruning tracked non-file descendants. */
48
91
  export function makeStructureUniverse(inventory) {
49
92
  const trackedNonFilePaths = new Set(inventory.trackedNonFilePaths);