@habitat-ai/resource-rule-evaluation 0.2.4 → 0.3.1

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,27 @@ 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) => materializeProgram(program, 0)),
86
+ ];
87
+ const evaluations = yield* Effect.forEach(programs, (program) => Effect.scoped(withScopedGritCatalog([program], (catalog) => runGritCheck(config, catalog, [program], input.subjectPaths))), { concurrency: 1 });
88
+ return Object.freeze({
89
+ results: Object.freeze(evaluations.flatMap(({ results }) => results)),
90
+ });
75
91
  });
76
92
  return Object.freeze({ evaluate });
77
93
  }
@@ -82,7 +98,7 @@ export function makeNodeGritRuleEvaluationResource(config) {
82
98
  evaluate: (input) => resource.evaluate(input).pipe(Effect.provide(NodeServices.layer)),
83
99
  });
84
100
  }
85
- function withScopedGritCatalog(program, use) {
101
+ function withScopedGritCatalog(programs, use) {
86
102
  return Effect.gen(function* () {
87
103
  const fs = yield* FileSystem.FileSystem;
88
104
  const path = yield* Path.Path;
@@ -105,12 +121,13 @@ function withScopedGritCatalog(program, use) {
105
121
  .makeDirectory(catalog.cacheDirectory)
106
122
  .pipe(mapPlatform("SetupFailed", "Failed to create Grit cache directory"));
107
123
  yield* fs
108
- .writeFileString(path.join(catalog.gritDirectory, "grit.yaml"), renderGritCatalog(program))
124
+ .writeFileString(path.join(catalog.gritDirectory, "grit.yaml"), renderGritCatalog(programs))
109
125
  .pipe(mapPlatform("SetupFailed", "Failed to write temporary Grit catalog"));
110
126
  return yield* use(catalog);
111
127
  });
112
128
  }
113
- function runGritCheck(config, catalog, subjectPaths) {
129
+ function runGritCheck(config, catalog, programs, subjectPaths) {
130
+ const outputLimit = gritOutputLimit(programs.length);
114
131
  const observe = Effect.gen(function* () {
115
132
  const command = ChildProcess.make(config.executable, ["--json", "check", "--no-cache", "--grit-dir", catalog.gritDirectory, ...subjectPaths], {
116
133
  cwd: catalog.root,
@@ -120,6 +137,7 @@ function runGritCheck(config, catalog, subjectPaths) {
120
137
  GRIT_USER_CONFIG: catalog.userConfigDirectory,
121
138
  GRIT_TELEMETRY_DISABLED: "true",
122
139
  NO_COLOR: undefined,
140
+ RAYON_NUM_THREADS: "2",
123
141
  },
124
142
  extendEnv: true,
125
143
  stdin: "ignore",
@@ -128,8 +146,8 @@ function runGritCheck(config, catalog, subjectPaths) {
128
146
  });
129
147
  const process = yield* command.pipe(mapPlatform("ExecutionFailed", "Failed to start Grit"));
130
148
  const output = yield* Effect.all({
131
- stdout: collectBoundedOutput(process.stdout, "stdout"),
132
- stderr: collectBoundedOutput(process.stderr, "stderr"),
149
+ stdout: collectBoundedOutput(process.stdout, "stdout", outputLimit),
150
+ stderr: collectBoundedOutput(process.stderr, "stderr", outputLimit),
133
151
  exitCode: process.exitCode.pipe(Effect.map(Number), mapPlatform("ExecutionFailed", "Failed to observe Grit exit")),
134
152
  }, { concurrency: "unbounded" });
135
153
  return Object.freeze(output);
@@ -147,7 +165,7 @@ function runGritCheck(config, catalog, subjectPaths) {
147
165
  if (observation.stdout.trim().length > 0) {
148
166
  return fail("InvalidOutput", "Grit check emitted unexpected stdout alongside its stderr JSON report");
149
167
  }
150
- return decodeGritReport(observation.stderr).pipe(Effect.map(resultFromGritReport));
168
+ return decodeGritReport(observation.stderr).pipe(Effect.flatMap((report) => resultFromGritReport(report, programs)));
151
169
  }));
152
170
  }
153
171
  function decodeGritReport(stderr) {
@@ -159,47 +177,61 @@ function decodeGritReport(stderr) {
159
177
  catch: (error) => failure("InvalidOutput", `Grit check emitted an invalid report: ${errorMessage(error)}`),
160
178
  })));
161
179
  }
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);
180
+ function resultFromGritReport(report, programs) {
181
+ const programsByPattern = new Map(programs.map((program) => [program.patternName, program]));
182
+ const findingsByProgram = new Map(programs.map(({ id }) => [id, []]));
183
+ for (const result of report.results) {
184
+ const program = programsByPattern.get(result.local_name);
185
+ if (program === undefined || !result.check_id.startsWith(`#${result.local_name}/`)) {
186
+ return fail("InvalidOutput", `Grit check reported unexpected pattern identity '${result.local_name}'`);
187
+ }
188
+ findingsByProgram.get(program.id)?.push(Object.freeze({
189
+ path: result.path,
190
+ start: Object.freeze({
191
+ line: result.start.line,
192
+ column: result.start.col,
193
+ offset: result.start.offset,
194
+ }),
195
+ end: Object.freeze({
196
+ line: result.end.line,
197
+ column: result.end.col,
198
+ offset: result.end.offset,
199
+ }),
200
+ message: result.extra.message,
201
+ }));
202
+ }
203
+ return Effect.succeed(Object.freeze({
204
+ results: Object.freeze(programs.map(({ id }) => Object.freeze({
205
+ programId: id,
206
+ findings: Object.freeze((findingsByProgram.get(id) ?? []).sort(compareFindings)),
207
+ }))),
208
+ }));
209
+ }
210
+ function materializeProgram(program, index) {
179
211
  return Object.freeze({
180
- findings: Object.freeze(findings),
212
+ id: program.id,
213
+ patternName: `${GRIT_PATTERN_PREFIX}${index}`,
214
+ program: program.program,
181
215
  });
182
216
  }
183
- function renderGritCatalog(program) {
217
+ function renderGritCatalog(programs) {
184
218
  return `${JSON.stringify({
185
219
  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
- ],
220
+ patterns: programs.map(({ id, patternName, program }) => Object.freeze({
221
+ name: patternName,
222
+ title: id,
223
+ level: "error",
224
+ body: program,
225
+ })),
194
226
  }, undefined, 2)}\n`;
195
227
  }
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) => {
228
+ function collectBoundedOutput(stream, channel, outputLimit) {
229
+ 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
230
  if (chunk.byteLength === 0)
199
231
  return Effect.succeed(state);
200
232
  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`)
233
+ return bytes > outputLimit
234
+ ? fail("InvalidOutput", `Grit ${channel} exceeded its ${outputLimit}-byte output limit`)
203
235
  : Effect.sync(() => {
204
236
  state.buffer.set(chunk, state.bytes);
205
237
  return Object.freeze({ buffer: state.buffer, bytes });
@@ -209,6 +241,9 @@ function collectBoundedOutput(stream, channel) {
209
241
  catch: (error) => failure("InvalidOutput", `Grit ${channel} was not valid UTF-8: ${errorMessage(error)}`),
210
242
  })));
211
243
  }
244
+ function gritOutputLimit(programCount) {
245
+ return Math.min(MAX_GRIT_BATCH_OUTPUT_BYTES, MAX_GRIT_OUTPUT_BYTES_PER_PROGRAM * programCount);
246
+ }
212
247
  function mapPlatform(reason, context) {
213
248
  return (effect) => effect.pipe(Effect.mapError((error) => failure(reason, `${context}: ${error.message}`)));
214
249
  }
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.1",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",