@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.
- package/dist/service/impl.d.ts +222 -2
- package/dist/service/modules/catalog/contract/catalog.d.ts +111 -1
- package/dist/service/modules/catalog/model/dto/catalog.d.ts +124 -5
- package/dist/service/modules/catalog/model/dto/catalog.js +141 -10
- package/dist/service/modules/catalog/model/dto/check.d.ts +60 -0
- package/dist/service/modules/catalog/model/dto/check.js +135 -98
- package/dist/service/modules/catalog/model/dto/structure.d.ts +18 -0
- package/dist/service/modules/catalog/model/dto/structure.js +39 -0
- package/dist/service/modules/catalog/model/policy/catalog.d.ts +16 -4
- package/dist/service/modules/catalog/model/policy/catalog.js +104 -9
- package/dist/service/modules/catalog/model/policy/check.d.ts +14 -2
- package/dist/service/modules/catalog/model/policy/check.js +70 -7
- package/dist/service/modules/catalog/model/policy/structure.d.ts +10 -4
- package/dist/service/modules/catalog/model/policy/structure.js +45 -2
- package/dist/service/modules/catalog/module.d.ts +222 -2
- package/dist/service/modules/catalog/router/catalog.router.d.ts +111 -1
- package/dist/service/modules/catalog/router/catalog.router.js +199 -8
- package/dist/service/modules/catalog/router.d.ts +111 -1
- package/dist/service/router.d.ts +111 -1
- package/package.json +9 -4
|
@@ -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.
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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 =
|
|
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
|
|
40
|
-
|
|
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",
|
|
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
|
|
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:
|
|
233
|
+
owner: applications.some((application) => application.ownerProject === value),
|
|
185
234
|
instance: catalog.applications.some((application) => application.instanceId === value),
|
|
186
|
-
rule:
|
|
187
|
-
runner:
|
|
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
|
-
|
|
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 =
|
|
16
|
-
type BoundStructureScope =
|
|
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({
|
|
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);
|