@habitat-ai/resource-rule-evaluation 0.2.4 → 0.3.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.
@@ -2,6 +2,11 @@ import type { Effect } from "effect";
2
2
  import { type Static, Type } from "typebox";
3
3
  /** Maximum detail retained by one rule-evaluation failure. */
4
4
  export declare const MAX_RULE_EVALUATION_FAILURE_DETAIL = 4096;
5
+ /** Structural schema for one caller-identified, already-resolved program. */
6
+ export declare const RuleEvaluationProgramSchema: Type.TObject<{
7
+ id: Type.TReadonly<Type.TString>;
8
+ program: Type.TReadonly<Type.TString>;
9
+ }>;
5
10
  /** Structural schema for one source position reported by an evaluator. */
6
11
  export declare const RuleEvaluationPositionSchema: Type.TObject<{
7
12
  line: Type.TReadonly<Type.TInteger>;
@@ -23,13 +28,17 @@ export declare const RuleEvaluationFindingSchema: Type.TObject<{
23
28
  }>>;
24
29
  message: Type.TReadonly<Type.TUnion<[Type.TString, Type.TNull]>>;
25
30
  }>;
26
- /** Structural schema for one already-resolved evaluation request. */
31
+ /** Structural schema for one exact-subject evaluation batch. */
27
32
  export declare const RuleEvaluationRequestSchema: Type.TObject<{
28
- program: Type.TReadonly<Type.TString>;
33
+ programs: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TObject<{
34
+ id: Type.TReadonly<Type.TString>;
35
+ program: Type.TReadonly<Type.TString>;
36
+ }>>>>;
29
37
  subjectPaths: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TString>>>;
30
38
  }>;
31
- /** Structural schema for one completed evaluation. */
32
- export declare const RuleEvaluationResultSchema: Type.TObject<{
39
+ /** Structural schema for one program's attributed evaluation result. */
40
+ export declare const RuleEvaluationProgramResultSchema: Type.TObject<{
41
+ programId: Type.TReadonly<Type.TString>;
33
42
  findings: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TObject<{
34
43
  path: Type.TReadonly<Type.TString>;
35
44
  start: Type.TReadonly<Type.TObject<{
@@ -45,6 +54,26 @@ export declare const RuleEvaluationResultSchema: Type.TObject<{
45
54
  message: Type.TReadonly<Type.TUnion<[Type.TString, Type.TNull]>>;
46
55
  }>>>>;
47
56
  }>;
57
+ /** Structural schema for one completed exact-subject evaluation batch. */
58
+ export declare const RuleEvaluationResultSchema: Type.TObject<{
59
+ results: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TObject<{
60
+ programId: Type.TReadonly<Type.TString>;
61
+ findings: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TObject<{
62
+ path: Type.TReadonly<Type.TString>;
63
+ start: Type.TReadonly<Type.TObject<{
64
+ line: Type.TReadonly<Type.TInteger>;
65
+ column: Type.TReadonly<Type.TInteger>;
66
+ offset: Type.TReadonly<Type.TInteger>;
67
+ }>>;
68
+ end: Type.TReadonly<Type.TObject<{
69
+ line: Type.TReadonly<Type.TInteger>;
70
+ column: Type.TReadonly<Type.TInteger>;
71
+ offset: Type.TReadonly<Type.TInteger>;
72
+ }>>;
73
+ message: Type.TReadonly<Type.TUnion<[Type.TString, Type.TNull]>>;
74
+ }>>>>;
75
+ }>>>>;
76
+ }>;
48
77
  /** Provider-neutral mechanical failure reasons for one evaluation attempt. */
49
78
  export declare const RuleEvaluationFailureReasonSchema: Type.TUnion<[Type.TLiteral<"InvalidInput">, Type.TLiteral<"SetupFailed">, Type.TLiteral<"ExecutionFailed">, Type.TLiteral<"TimedOut">, Type.TLiteral<"InvalidOutput">]>;
50
79
  /** Structural schema for one bounded typed evaluation failure. */
@@ -57,9 +86,13 @@ export declare const RuleEvaluationFailureSchema: Type.TObject<{
57
86
  export type RuleEvaluationPosition = Static<typeof RuleEvaluationPositionSchema>;
58
87
  /** One provider-neutral finding. */
59
88
  export type RuleEvaluationFinding = Static<typeof RuleEvaluationFindingSchema>;
60
- /** One already-resolved evaluation request. */
89
+ /** One caller-identified, already-resolved evaluator program. */
90
+ export type RuleEvaluationProgram = Static<typeof RuleEvaluationProgramSchema>;
91
+ /** One exact-subject evaluation batch. */
61
92
  export type RuleEvaluationRequest = Static<typeof RuleEvaluationRequestSchema>;
62
- /** One completed evaluation result. */
93
+ /** One completed program result within an evaluation batch. */
94
+ export type RuleEvaluationProgramResult = Static<typeof RuleEvaluationProgramResultSchema>;
95
+ /** One completed exact-subject evaluation batch. */
63
96
  export type RuleEvaluationResult = Static<typeof RuleEvaluationResultSchema>;
64
97
  /** One provider-neutral mechanical failure reason. */
65
98
  export type RuleEvaluationFailureReason = Static<typeof RuleEvaluationFailureReasonSchema>;
@@ -67,7 +100,7 @@ export type RuleEvaluationFailureReason = Static<typeof RuleEvaluationFailureRea
67
100
  export type RuleEvaluationFailure = Static<typeof RuleEvaluationFailureSchema>;
68
101
  /** Checks an unknown value against the complete rule-evaluation failure contract. */
69
102
  export declare function isRuleEvaluationFailure(input: unknown): input is RuleEvaluationFailure;
70
- /** Provider-neutral capability for evaluating one resolved program. */
103
+ /** Provider-neutral capability for evaluating resolved programs over one exact subject set. */
71
104
  export interface RuleEvaluationResource<R = never> {
72
105
  readonly evaluate: (input: RuleEvaluationRequest) => Effect.Effect<RuleEvaluationResult, RuleEvaluationFailure, R>;
73
106
  }
package/dist/contract.js CHANGED
@@ -5,6 +5,18 @@ export const MAX_RULE_EVALUATION_FAILURE_DETAIL = 4_096;
5
5
  const NonEmptyStringSchema = Type.String({
6
6
  minLength: 1,
7
7
  });
8
+ /** Structural schema for one caller-identified, already-resolved program. */
9
+ export const RuleEvaluationProgramSchema = ReadonlyObject(Type.Object({
10
+ id: Type.String({
11
+ minLength: 1,
12
+ maxLength: 1_024,
13
+ description: "Invocation-local program identity used to attribute findings",
14
+ }),
15
+ program: Type.String({
16
+ minLength: 1,
17
+ description: "Already-resolved evaluator program",
18
+ }),
19
+ }), { additionalProperties: false });
8
20
  /** Structural schema for one source position reported by an evaluator. */
9
21
  export const RuleEvaluationPositionSchema = ReadonlyObject(Type.Object({
10
22
  line: Type.Integer({
@@ -32,23 +44,35 @@ export const RuleEvaluationFindingSchema = ReadonlyObject(Type.Object({
32
44
  description: "Evaluator-authored diagnostic message when one was produced",
33
45
  }),
34
46
  }), { additionalProperties: false });
35
- /** Structural schema for one already-resolved evaluation request. */
47
+ /** Structural schema for one exact-subject evaluation batch. */
36
48
  export const RuleEvaluationRequestSchema = ReadonlyObject(Type.Object({
37
- program: Type.String({
38
- minLength: 1,
39
- description: "Already-resolved evaluator program",
49
+ programs: ReadonlyObject(Type.Array(RuleEvaluationProgramSchema), {
50
+ minItems: 1,
51
+ description: "Non-empty ordered programs evaluated against the same subjects",
40
52
  }),
41
53
  subjectPaths: ReadonlyObject(Type.Array(NonEmptyStringSchema), {
42
54
  minItems: 1,
43
55
  description: "Non-empty caller-resolved absolute subject paths",
44
56
  }),
45
57
  }), { additionalProperties: false });
46
- /** Structural schema for one completed evaluation. */
47
- export const RuleEvaluationResultSchema = ReadonlyObject(Type.Object({
58
+ /** Structural schema for one program's attributed evaluation result. */
59
+ export const RuleEvaluationProgramResultSchema = ReadonlyObject(Type.Object({
60
+ programId: Type.String({
61
+ minLength: 1,
62
+ maxLength: 1_024,
63
+ description: "Invocation-local identity of the evaluated program",
64
+ }),
48
65
  findings: ReadonlyObject(Type.Array(RuleEvaluationFindingSchema), {
49
66
  description: "Findings reported by the evaluator in provider-defined stable order",
50
67
  }),
51
68
  }), { additionalProperties: false });
69
+ /** Structural schema for one completed exact-subject evaluation batch. */
70
+ export const RuleEvaluationResultSchema = ReadonlyObject(Type.Object({
71
+ results: ReadonlyObject(Type.Array(RuleEvaluationProgramResultSchema), {
72
+ minItems: 1,
73
+ description: "Per-program results in request order",
74
+ }),
75
+ }), { additionalProperties: false });
52
76
  /** Provider-neutral mechanical failure reasons for one evaluation attempt. */
53
77
  export const RuleEvaluationFailureReasonSchema = Type.Union([
54
78
  Type.Literal("InvalidInput"),
@@ -6,9 +6,10 @@ import { ReadonlyObject, Type } from "typebox";
6
6
  import Schema from "typebox/schema";
7
7
  import { Value } from "typebox/value";
8
8
  const TEMP_CATALOG_PREFIX = "habitat-rule-evaluation-";
9
- const GRIT_PATTERN_NAME = "habitat_rule_evaluation";
10
- /** Maximum bytes retained from either native output stream. */
11
- const MAX_GRIT_OUTPUT_BYTES = 256 * 1_024;
9
+ const GRIT_PATTERN_PREFIX = "habitat_rule_evaluation_";
10
+ /** Output retained per program, capped so an untrusted batch cannot allocate without bound. */
11
+ const MAX_GRIT_OUTPUT_BYTES_PER_PROGRAM = 256 * 1_024;
12
+ const MAX_GRIT_BATCH_OUTPUT_BYTES = 16 * 1_024 * 1_024;
12
13
  /** Structural schema for Grit provider construction. */
13
14
  export const GritRuleEvaluationProviderConfigSchema = ReadonlyObject(Type.Object({
14
15
  executable: Type.String({
@@ -27,11 +28,12 @@ const GritPositionSchema = Type.Object({
27
28
  }, { additionalProperties: false, description: "Grit-reported source position" });
28
29
  const GritResultSchema = Type.Object({
29
30
  check_id: Type.String({
30
- pattern: `^#${GRIT_PATTERN_NAME}/.+$`,
31
- description: "Provider-owned Grit check identity with evaluator suffix",
31
+ minLength: 1,
32
+ description: "Grit check identity with evaluator suffix",
32
33
  }),
33
- local_name: Type.Literal(GRIT_PATTERN_NAME, {
34
- description: "Provider-owned local Grit pattern name",
34
+ local_name: Type.String({
35
+ minLength: 1,
36
+ description: "Local Grit catalog pattern name",
35
37
  }),
36
38
  path: Type.String({ minLength: 1, description: "Grit-reported subject path" }),
37
39
  start: GritPositionSchema,
@@ -65,13 +67,24 @@ export function makeGritRuleEvaluationResource(config) {
65
67
  return yield* fail("InvalidInput", "Grit provider configuration does not match its structural contract");
66
68
  }
67
69
  if (!requestValidator.Check(input)) {
68
- return yield* fail("InvalidInput", "Rule-evaluation request requires one resolved program and non-empty subject paths");
70
+ return yield* fail("InvalidInput", "Rule-evaluation request requires resolved programs and non-empty subject paths");
69
71
  }
70
72
  const path = yield* Path.Path;
71
73
  if (input.subjectPaths.some((subjectPath) => !path.isAbsolute(subjectPath))) {
72
74
  return yield* fail("InvalidInput", "Rule-evaluation subject paths must be caller-resolved absolute paths");
73
75
  }
74
- return yield* Effect.scoped(withScopedGritCatalog(input.program, (catalog) => runGritCheck(config, catalog, input.subjectPaths)));
76
+ const [firstProgram, ...remainingPrograms] = input.programs;
77
+ if (firstProgram === undefined) {
78
+ return yield* fail("InvalidInput", "Rule-evaluation request requires at least one program");
79
+ }
80
+ if (new Set(input.programs.map(({ id }) => id)).size !== input.programs.length) {
81
+ return yield* fail("InvalidInput", "Rule-evaluation program identities must be unique");
82
+ }
83
+ const programs = [
84
+ materializeProgram(firstProgram, 0),
85
+ ...remainingPrograms.map((program, index) => materializeProgram(program, index + 1)),
86
+ ];
87
+ return yield* Effect.scoped(withScopedGritCatalog(programs, (catalog) => runGritCheck(config, catalog, programs, input.subjectPaths)));
75
88
  });
76
89
  return Object.freeze({ evaluate });
77
90
  }
@@ -82,7 +95,7 @@ export function makeNodeGritRuleEvaluationResource(config) {
82
95
  evaluate: (input) => resource.evaluate(input).pipe(Effect.provide(NodeServices.layer)),
83
96
  });
84
97
  }
85
- function withScopedGritCatalog(program, use) {
98
+ function withScopedGritCatalog(programs, use) {
86
99
  return Effect.gen(function* () {
87
100
  const fs = yield* FileSystem.FileSystem;
88
101
  const path = yield* Path.Path;
@@ -105,12 +118,13 @@ function withScopedGritCatalog(program, use) {
105
118
  .makeDirectory(catalog.cacheDirectory)
106
119
  .pipe(mapPlatform("SetupFailed", "Failed to create Grit cache directory"));
107
120
  yield* fs
108
- .writeFileString(path.join(catalog.gritDirectory, "grit.yaml"), renderGritCatalog(program))
121
+ .writeFileString(path.join(catalog.gritDirectory, "grit.yaml"), renderGritCatalog(programs))
109
122
  .pipe(mapPlatform("SetupFailed", "Failed to write temporary Grit catalog"));
110
123
  return yield* use(catalog);
111
124
  });
112
125
  }
113
- function runGritCheck(config, catalog, subjectPaths) {
126
+ function runGritCheck(config, catalog, programs, subjectPaths) {
127
+ const outputLimit = gritOutputLimit(programs.length);
114
128
  const observe = Effect.gen(function* () {
115
129
  const command = ChildProcess.make(config.executable, ["--json", "check", "--no-cache", "--grit-dir", catalog.gritDirectory, ...subjectPaths], {
116
130
  cwd: catalog.root,
@@ -120,6 +134,7 @@ function runGritCheck(config, catalog, subjectPaths) {
120
134
  GRIT_USER_CONFIG: catalog.userConfigDirectory,
121
135
  GRIT_TELEMETRY_DISABLED: "true",
122
136
  NO_COLOR: undefined,
137
+ RAYON_NUM_THREADS: "2",
123
138
  },
124
139
  extendEnv: true,
125
140
  stdin: "ignore",
@@ -128,8 +143,8 @@ function runGritCheck(config, catalog, subjectPaths) {
128
143
  });
129
144
  const process = yield* command.pipe(mapPlatform("ExecutionFailed", "Failed to start Grit"));
130
145
  const output = yield* Effect.all({
131
- stdout: collectBoundedOutput(process.stdout, "stdout"),
132
- stderr: collectBoundedOutput(process.stderr, "stderr"),
146
+ stdout: collectBoundedOutput(process.stdout, "stdout", outputLimit),
147
+ stderr: collectBoundedOutput(process.stderr, "stderr", outputLimit),
133
148
  exitCode: process.exitCode.pipe(Effect.map(Number), mapPlatform("ExecutionFailed", "Failed to observe Grit exit")),
134
149
  }, { concurrency: "unbounded" });
135
150
  return Object.freeze(output);
@@ -147,7 +162,7 @@ function runGritCheck(config, catalog, subjectPaths) {
147
162
  if (observation.stdout.trim().length > 0) {
148
163
  return fail("InvalidOutput", "Grit check emitted unexpected stdout alongside its stderr JSON report");
149
164
  }
150
- return decodeGritReport(observation.stderr).pipe(Effect.map(resultFromGritReport));
165
+ return decodeGritReport(observation.stderr).pipe(Effect.flatMap((report) => resultFromGritReport(report, programs)));
151
166
  }));
152
167
  }
153
168
  function decodeGritReport(stderr) {
@@ -159,47 +174,61 @@ function decodeGritReport(stderr) {
159
174
  catch: (error) => failure("InvalidOutput", `Grit check emitted an invalid report: ${errorMessage(error)}`),
160
175
  })));
161
176
  }
162
- function resultFromGritReport(report) {
163
- const findings = report.results
164
- .map((result) => Object.freeze({
165
- path: result.path,
166
- start: Object.freeze({
167
- line: result.start.line,
168
- column: result.start.col,
169
- offset: result.start.offset,
170
- }),
171
- end: Object.freeze({
172
- line: result.end.line,
173
- column: result.end.col,
174
- offset: result.end.offset,
175
- }),
176
- message: result.extra.message,
177
- }))
178
- .sort(compareFindings);
177
+ function resultFromGritReport(report, programs) {
178
+ const programsByPattern = new Map(programs.map((program) => [program.patternName, program]));
179
+ const findingsByProgram = new Map(programs.map(({ id }) => [id, []]));
180
+ for (const result of report.results) {
181
+ const program = programsByPattern.get(result.local_name);
182
+ if (program === undefined || !result.check_id.startsWith(`#${result.local_name}/`)) {
183
+ return fail("InvalidOutput", `Grit check reported unexpected pattern identity '${result.local_name}'`);
184
+ }
185
+ findingsByProgram.get(program.id)?.push(Object.freeze({
186
+ path: result.path,
187
+ start: Object.freeze({
188
+ line: result.start.line,
189
+ column: result.start.col,
190
+ offset: result.start.offset,
191
+ }),
192
+ end: Object.freeze({
193
+ line: result.end.line,
194
+ column: result.end.col,
195
+ offset: result.end.offset,
196
+ }),
197
+ message: result.extra.message,
198
+ }));
199
+ }
200
+ return Effect.succeed(Object.freeze({
201
+ results: Object.freeze(programs.map(({ id }) => Object.freeze({
202
+ programId: id,
203
+ findings: Object.freeze((findingsByProgram.get(id) ?? []).sort(compareFindings)),
204
+ }))),
205
+ }));
206
+ }
207
+ function materializeProgram(program, index) {
179
208
  return Object.freeze({
180
- findings: Object.freeze(findings),
209
+ id: program.id,
210
+ patternName: `${GRIT_PATTERN_PREFIX}${index}`,
211
+ program: program.program,
181
212
  });
182
213
  }
183
- function renderGritCatalog(program) {
214
+ function renderGritCatalog(programs) {
184
215
  return `${JSON.stringify({
185
216
  version: "0.0.2",
186
- patterns: [
187
- {
188
- name: GRIT_PATTERN_NAME,
189
- title: GRIT_PATTERN_NAME,
190
- level: "error",
191
- body: program,
192
- },
193
- ],
217
+ patterns: programs.map(({ id, patternName, program }) => Object.freeze({
218
+ name: patternName,
219
+ title: id,
220
+ level: "error",
221
+ body: program,
222
+ })),
194
223
  }, undefined, 2)}\n`;
195
224
  }
196
- function collectBoundedOutput(stream, channel) {
197
- return Stream.runFoldEffect(stream.pipe(Stream.mapError((error) => failure("ExecutionFailed", `Failed to drain Grit ${channel}: ${error.message}`))), () => Object.freeze({ buffer: new Uint8Array(MAX_GRIT_OUTPUT_BYTES), bytes: 0 }), (state, chunk) => {
225
+ function collectBoundedOutput(stream, channel, outputLimit) {
226
+ return Stream.runFoldEffect(stream.pipe(Stream.mapError((error) => failure("ExecutionFailed", `Failed to drain Grit ${channel}: ${error.message}`))), () => Object.freeze({ buffer: new Uint8Array(outputLimit), bytes: 0 }), (state, chunk) => {
198
227
  if (chunk.byteLength === 0)
199
228
  return Effect.succeed(state);
200
229
  const bytes = state.bytes + chunk.byteLength;
201
- return bytes > MAX_GRIT_OUTPUT_BYTES
202
- ? fail("InvalidOutput", `Grit ${channel} exceeded its ${MAX_GRIT_OUTPUT_BYTES}-byte output limit`)
230
+ return bytes > outputLimit
231
+ ? fail("InvalidOutput", `Grit ${channel} exceeded its ${outputLimit}-byte output limit`)
203
232
  : Effect.sync(() => {
204
233
  state.buffer.set(chunk, state.bytes);
205
234
  return Object.freeze({ buffer: state.buffer, bytes });
@@ -209,6 +238,9 @@ function collectBoundedOutput(stream, channel) {
209
238
  catch: (error) => failure("InvalidOutput", `Grit ${channel} was not valid UTF-8: ${errorMessage(error)}`),
210
239
  })));
211
240
  }
241
+ function gritOutputLimit(programCount) {
242
+ return Math.min(MAX_GRIT_BATCH_OUTPUT_BYTES, MAX_GRIT_OUTPUT_BYTES_PER_PROGRAM * programCount);
243
+ }
212
244
  function mapPlatform(reason, context) {
213
245
  return (effect) => effect.pipe(Effect.mapError((error) => failure(reason, `${context}: ${error.message}`)));
214
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@habitat-ai/resource-rule-evaluation",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",