@ontrails/warden 1.0.0-beta.45 → 1.0.0-beta.46

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @ontrails/warden
2
2
 
3
+ ## 1.0.0-beta.46
4
+
5
+ ### Minor Changes
6
+
7
+ - [`fb0ba0a`](https://github.com/outfitter-dev/trails/commit/fb0ba0ab706bbdce470123e9a6fb2ef9f1822806): Convert the eight committed governed Regrade histories to canonical v3 receipts and remove the temporary schema-v2 compatibility path after migration.
8
+ - [`9a8b6e4`](https://github.com/outfitter-dev/trails/commit/9a8b6e4af394c76c11e6d0007e0f5f94d0be2cb3): Persist Regrade lifecycle runs as canonical v3 receipts with exact Git blob evidence and authored field provenance, and validate their compact classified-form projection independently in Warden.
9
+
10
+ ### Patch Changes
11
+
12
+ - [`54d259b`](https://github.com/outfitter-dev/trails/commit/54d259be81fb6c41d85be48a6cb2100c746a7126): Expose parser-native comment spans from `parseWithDiagnostics` so source-aware
13
+ tooling can distinguish exact JavaScript and TypeScript comment trivia without
14
+ reimplementing a lexer.
15
+
16
+ Use the shared spans in Warden's public-example rule while keeping leading
17
+ comment ownership fail-closed across JavaScript line terminators.
18
+
19
+ - [`768cc79`](https://github.com/outfitter-dev/trails/commit/768cc79ca10947b8808b376e281e1a81131b4acc): Close missed projection vocabulary residue in Regrade internals and public
20
+ error-rendering guidance, and keep lifecycle-ambiguous governed identifiers in
21
+ the Warden review inventory instead of assigning them an unsafe automatic
22
+ target.
23
+
3
24
  ## 1.0.0-beta.45
4
25
 
5
26
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/warden",
3
- "version": "1.0.0-beta.45",
3
+ "version": "1.0.0-beta.46",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/outfitter-dev/trails.git",
@@ -32,20 +32,20 @@
32
32
  "clean": "rm -rf dist *.tsbuildinfo"
33
33
  },
34
34
  "dependencies": {
35
- "@ontrails/adapter-kit": "^1.0.0-beta.45",
36
- "@ontrails/cli": "^1.0.0-beta.45",
37
- "@ontrails/config": "^1.0.0-beta.45",
38
- "@ontrails/permits": "^1.0.0-beta.45",
39
- "@ontrails/source": "^1.0.0-beta.45",
40
- "@ontrails/store": "^1.0.0-beta.45",
35
+ "@ontrails/adapter-kit": "^1.0.0-beta.46",
36
+ "@ontrails/cli": "^1.0.0-beta.46",
37
+ "@ontrails/config": "^1.0.0-beta.46",
38
+ "@ontrails/permits": "^1.0.0-beta.46",
39
+ "@ontrails/source": "^1.0.0-beta.46",
40
+ "@ontrails/store": "^1.0.0-beta.46",
41
41
  "oxc-resolver": "11.19.1",
42
42
  "zod": "^4.3.5"
43
43
  },
44
44
  "devDependencies": {
45
- "@ontrails/testing": "^1.0.0-beta.45"
45
+ "@ontrails/testing": "^1.0.0-beta.46"
46
46
  },
47
47
  "peerDependencies": {
48
- "@ontrails/core": "^1.0.0-beta.45",
49
- "@ontrails/topography": "^1.0.0-beta.45"
48
+ "@ontrails/core": "^1.0.0-beta.46",
49
+ "@ontrails/topography": "^1.0.0-beta.46"
50
50
  }
51
51
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  export type {
12
12
  BuiltinWardenRuleName,
13
13
  GovernedVocabularyHistoryEvidence,
14
- GovernedVocabularyHistoryFormObservation,
14
+ GovernedVocabularyHistoryFormJudgment,
15
15
  GovernedVocabularyHistoryIssue,
16
16
  ProjectAwareWardenRule,
17
17
  ProjectContext,
@@ -1,345 +1,16 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join, relative } from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { z } from 'zod';
5
3
 
6
4
  import {
7
5
  getGovernedVocabularyTransition,
8
- governedVocabularyHistoryProvenanceSchema,
9
6
  listGovernedVocabularyTransitions,
10
7
  } from './rules/retired-vocabulary.js';
8
+ import { resolveReceiptForWarden } from './regrade-receipt.js';
11
9
  import type {
12
10
  GovernedVocabularyHistoryEvidence,
13
11
  GovernedVocabularyHistoryIssue,
14
12
  } from './rules/types.js';
15
13
 
16
- const planBodySchema = z.discriminatedUnion('kind', [
17
- z
18
- .object({
19
- caseSensitive: z.boolean().optional(),
20
- from: z.string().min(1),
21
- id: z.string().optional(),
22
- kind: z.literal('vocabulary'),
23
- to: z.string().min(1),
24
- })
25
- .passthrough(),
26
- z
27
- .object({
28
- classIds: z.array(z.string().min(1)).min(1),
29
- id: z.string().min(1),
30
- kind: z.literal('class'),
31
- })
32
- .passthrough(),
33
- ]);
34
-
35
- const planArtifactSchema = z
36
- .object({
37
- derivation: z.unknown().optional(),
38
- expansion: z.unknown().optional(),
39
- kind: z.literal('regrade-plan'),
40
- path: z.string().min(1),
41
- plan: planBodySchema,
42
- provenance: z.object({
43
- fields: z.record(z.string(), z.enum(['authored', 'derived'])),
44
- }),
45
- schemaVersion: z.literal(1),
46
- sourceHash: z.string().min(1),
47
- transitionId: z.string().min(1).optional(),
48
- })
49
- .strict();
50
-
51
- const historyRunSchema = z
52
- .object({
53
- completionReport: z.unknown().optional(),
54
- completionReportHash: z.string().optional(),
55
- lockHashAtRun: z.string().min(1),
56
- plan: planArtifactSchema,
57
- planContentHash: z.string().min(1),
58
- provenance: governedVocabularyHistoryProvenanceSchema.optional(),
59
- report: z.unknown(),
60
- })
61
- .strict();
62
-
63
- const historyArtifactSchema = z
64
- .object({
65
- id: z.string().min(1),
66
- kind: z.literal('regrade-history'),
67
- path: z.string().min(1),
68
- runs: z.array(historyRunSchema).min(1),
69
- schemaVersion: z.literal(2),
70
- })
71
- .strict();
72
-
73
- const reportEntrySchema = z
74
- .object({
75
- classId: z.string().optional(),
76
- notes: z.array(z.string()).optional(),
77
- outcome: z.enum(['needs-review', 'no-op', 'rewrite', 'skip']),
78
- path: z.string(),
79
- reason: z.string().optional(),
80
- reviewDetails: z.unknown().optional(),
81
- })
82
- .passthrough();
83
-
84
- const vocabularyDispositionSchema = z.enum([
85
- 'code-context-out-of-engine',
86
- 'docs-only',
87
- 'explicit-preserve',
88
- 'forward-pointer',
89
- 'historical-by-policy',
90
- 'ignored-by-scope',
91
- 'in-family-modified',
92
- 'in-family-unresolved',
93
- 'out-of-family',
94
- 'preserve-current-live-api',
95
- ]);
96
-
97
- const historyOccurrenceSchema = z
98
- .object({
99
- disposition: vocabularyDispositionSchema.optional(),
100
- form: z.string(),
101
- line: z.number().int().positive().optional(),
102
- path: z.string(),
103
- reason: z.string().optional(),
104
- scopeTier: z.string().nullish(),
105
- verdict: z.enum(['applied', 'deferred', 'modified', 'skipped']).optional(),
106
- })
107
- .passthrough()
108
- .superRefine((occurrence, ctx) => {
109
- const modernOccurrence =
110
- occurrence.disposition !== undefined &&
111
- occurrence.line !== undefined &&
112
- occurrence.reason !== undefined &&
113
- occurrence.verdict !== undefined;
114
- if (
115
- modernOccurrence &&
116
- occurrence.scopeTier !== undefined &&
117
- occurrence.scopeTier !== null &&
118
- occurrence.scopeTier !== 'in-scope' &&
119
- occurrence.scopeTier !== 'policy-classified'
120
- ) {
121
- ctx.addIssue({
122
- code: 'custom',
123
- message: 'Modern Regrade occurrences use a recognized scope tier.',
124
- path: ['scopeTier'],
125
- });
126
- }
127
- });
128
-
129
- const governedObservationSchema = historyOccurrenceSchema.safeExtend({
130
- disposition: vocabularyDispositionSchema,
131
- line: z.number().int().positive(),
132
- reason: z.string(),
133
- scopeTier: z.enum(['in-scope', 'policy-classified']).nullish(),
134
- verdict: z.enum(['applied', 'deferred', 'modified', 'skipped']),
135
- });
136
-
137
- const reportSchema = z
138
- .object({
139
- apply: z.object({ applied: z.number() }).passthrough().optional(),
140
- entries: z.array(reportEntrySchema),
141
- matched: z.number(),
142
- review: z.number(),
143
- rewritten: z.number(),
144
- root: z.string(),
145
- run: z
146
- .object({
147
- ledger: z
148
- .object({
149
- cycle: z.unknown(),
150
- forms: z.record(z.string(), z.unknown()),
151
- occurrences: z.array(historyOccurrenceSchema),
152
- })
153
- .passthrough(),
154
- report: z
155
- .object({
156
- fileRenames: z
157
- .array(
158
- z
159
- .object({
160
- deferred: z.number(),
161
- from: z.string(),
162
- historical: z.number().optional(),
163
- preserved: z.number().optional(),
164
- rewritten: z.number(),
165
- skipped: z.number().optional(),
166
- to: z.string(),
167
- })
168
- .passthrough()
169
- )
170
- .optional(),
171
- })
172
- .passthrough(),
173
- })
174
- .passthrough()
175
- .optional(),
176
- scan: z.object({
177
- byDirectory: z.array(z.unknown()),
178
- byExtension: z.array(z.unknown()),
179
- files: z.object({
180
- matched: z.number(),
181
- scanned: z.number(),
182
- skipped: z.number(),
183
- }),
184
- skippedByReason: z.record(z.string(), z.number()),
185
- }),
186
- scanned: z.number(),
187
- selectedClassIds: z.array(z.string()),
188
- skipped: z.number(),
189
- skipsByReason: z.record(z.string(), z.number()),
190
- unknownClassIds: z.array(z.string()),
191
- })
192
- .passthrough();
193
-
194
- const governedHistoryRunSchema = historyRunSchema.extend({
195
- completionReport: reportSchema,
196
- completionReportHash: z.string().regex(/^[0-9a-f]{64}$/),
197
- lockHashAtRun: z.string().regex(/^[0-9a-f]{64}$/),
198
- planContentHash: z.string().regex(/^[0-9a-f]{64}$/),
199
- report: reportSchema,
200
- });
201
-
202
- type GovernedHistoryRun = z.infer<typeof governedHistoryRunSchema>;
203
- type HistoryRun = z.infer<typeof historyRunSchema>;
204
-
205
- const isPlainObject = (value: unknown): value is Record<string, unknown> =>
206
- typeof value === 'object' && value !== null && !Array.isArray(value);
207
-
208
- const canonicalizeJsonValue = (value: unknown): unknown => {
209
- if (Array.isArray(value)) {
210
- return value.map(canonicalizeJsonValue);
211
- }
212
- if (isPlainObject(value)) {
213
- return Object.fromEntries(
214
- Object.keys(value)
215
- .toSorted()
216
- .map((key) => [key, canonicalizeJsonValue(value[key])])
217
- );
218
- }
219
- return value;
220
- };
221
-
222
- const hash = (value: string): string =>
223
- createHash('sha256').update(value).digest('hex');
224
-
225
- const isGeneratedRegradeArtifact = (path: string): boolean =>
226
- /(?:^|\/)\.trails\/regrade\/.+\.json$/u.test(path);
227
-
228
- const sourceHashFacts = (
229
- report: z.infer<typeof reportSchema>,
230
- policyMode: 'current' | 'legacy' = 'current',
231
- fileEvidenceMode: 'current' | 'legacy' = 'current'
232
- ): unknown => {
233
- const ledger = report.run?.ledger;
234
- const occurrences = ledger?.occurrences.filter(
235
- (occurrence) =>
236
- occurrence.scopeTier !== 'policy-classified' ||
237
- (policyMode === 'current' && !isGeneratedRegradeArtifact(occurrence.path))
238
- );
239
- const forms = new Set(occurrences?.map((occurrence) => occurrence.form));
240
- return {
241
- entries: report.entries
242
- .filter(
243
- (entry) =>
244
- entry.outcome === 'rewrite' || entry.outcome === 'needs-review'
245
- )
246
- .map(({ classId, notes, outcome, path, reason, reviewDetails }) => ({
247
- ...(classId === undefined ? {} : { classId }),
248
- ...(notes === undefined ? {} : { notes }),
249
- outcome,
250
- path,
251
- ...(reason === undefined ? {} : { reason }),
252
- ...(reviewDetails === undefined ? {} : { reviewDetails }),
253
- })),
254
- fileRenames: report.run?.report.fileRenames?.map((rename) => ({
255
- deferred: rename.deferred,
256
- from: rename.from,
257
- ...(fileEvidenceMode === 'current'
258
- ? {
259
- historical: rename.historical ?? 0,
260
- preserved: rename.preserved ?? 0,
261
- }
262
- : {}),
263
- rewritten: rename.rewritten,
264
- ...(fileEvidenceMode === 'current'
265
- ? { skipped: rename.skipped ?? 0 }
266
- : {}),
267
- to: rename.to,
268
- })),
269
- ledger:
270
- ledger === undefined || occurrences === undefined || forms === undefined
271
- ? undefined
272
- : {
273
- cycle: ledger.cycle,
274
- forms: Object.fromEntries(
275
- Object.entries(ledger.forms).filter(([form]) => forms.has(form))
276
- ),
277
- occurrences,
278
- },
279
- selectedClassIds: report.selectedClassIds,
280
- };
281
- };
282
-
283
- const sourceHashes = (report: z.infer<typeof reportSchema>): Set<string> => {
284
- const facts = [
285
- sourceHashFacts(report),
286
- sourceHashFacts(report, 'current', 'legacy'),
287
- sourceHashFacts(report, 'legacy'),
288
- sourceHashFacts(report, 'legacy', 'legacy'),
289
- ];
290
- return new Set(
291
- facts.flatMap((value) => [
292
- hash(JSON.stringify(canonicalizeJsonValue(value))),
293
- hash(JSON.stringify(value)),
294
- ])
295
- );
296
- };
297
-
298
- interface RawGovernedHistoryRun {
299
- readonly completionReport?: z.infer<typeof reportSchema>;
300
- readonly report: z.infer<typeof reportSchema>;
301
- }
302
-
303
- const rawEvidenceReports = (
304
- run: GovernedHistoryRun,
305
- rawRun: RawGovernedHistoryRun | undefined
306
- ): {
307
- readonly completionReport: z.infer<typeof reportSchema>;
308
- readonly report: z.infer<typeof reportSchema>;
309
- } => ({
310
- completionReport:
311
- rawRun?.completionReport ?? rawRun?.report ?? run.completionReport,
312
- report: rawRun?.report ?? run.report,
313
- });
314
-
315
- const validatesDeterministicEvidence = (
316
- run: GovernedHistoryRun,
317
- rawRun: RawGovernedHistoryRun | undefined,
318
- transitionId: string
319
- ): boolean => {
320
- const { provenance } = run;
321
- const { completionReport, report } = rawEvidenceReports(run, rawRun);
322
- const provenanceValid =
323
- provenance === undefined ||
324
- (provenance.disposition ===
325
- (run.report.review > 0 ? 'review-follow-up' : 'applied-clean') &&
326
- provenance.kind === 'governed-vocabulary' &&
327
- provenance.planContentHash === run.planContentHash &&
328
- provenance.reviewPending === run.report.review &&
329
- provenance.safeApplied ===
330
- (run.report.apply?.applied ?? run.report.rewritten) &&
331
- sourceHashes(completionReport).has(provenance.sourceHashAfter) &&
332
- sourceHashes(report).has(provenance.sourceHashBefore) &&
333
- provenance.transitionId === transitionId);
334
- return (
335
- hash(JSON.stringify(canonicalizeJsonValue(run.plan.plan))) ===
336
- run.planContentHash &&
337
- sourceHashes(report).has(run.lockHashAtRun) &&
338
- sourceHashes(completionReport).has(run.completionReportHash) &&
339
- provenanceValid
340
- );
341
- };
342
-
343
14
  export interface GovernedVocabularyHistoryIndex {
344
15
  readonly byTransitionId: ReadonlyMap<
345
16
  string,
@@ -350,50 +21,6 @@ export interface GovernedVocabularyHistoryIndex {
350
21
 
351
22
  const normalizePath = (value: string): string => value.replaceAll('\\', '/');
352
23
 
353
- const latestRunFacts = (
354
- run: HistoryRun | undefined
355
- ): Pick<
356
- GovernedVocabularyHistoryEvidence,
357
- 'caseSensitive' | 'latestFormObservations'
358
- > => {
359
- if (run === undefined || run.plan.plan.kind !== 'vocabulary') {
360
- return { caseSensitive: false, latestFormObservations: [] };
361
- }
362
- const report = reportSchema.safeParse(run.completionReport ?? run.report);
363
- if (!report.success) {
364
- return {
365
- caseSensitive: run.plan.plan.caseSensitive === true,
366
- latestFormObservations: [],
367
- };
368
- }
369
- return {
370
- caseSensitive: run.plan.plan.caseSensitive === true,
371
- latestFormObservations: (report.data.run?.ledger.occurrences ?? []).flatMap(
372
- (occurrence) => {
373
- const observation = governedObservationSchema.safeParse(occurrence);
374
- if (!observation.success) {
375
- return [];
376
- }
377
- const { disposition, form, line, path, reason, scopeTier, verdict } =
378
- observation.data;
379
- return [
380
- {
381
- disposition,
382
- form,
383
- line,
384
- path,
385
- reason,
386
- ...(scopeTier === undefined || scopeTier === null
387
- ? {}
388
- : { scopeTier }),
389
- verdict,
390
- },
391
- ];
392
- }
393
- ),
394
- };
395
- };
396
-
397
24
  type HistoryFileResult =
398
25
  | {
399
26
  readonly kind: 'evidence';
@@ -415,17 +42,23 @@ const historyIssue = (
415
42
  },
416
43
  });
417
44
 
418
- const governedTransitionForRun = (run: z.infer<typeof historyRunSchema>) => {
419
- const { plan } = run.plan;
45
+ type GovernedReceiptPlan =
46
+ | { readonly kind: 'class' }
47
+ | {
48
+ readonly from: string;
49
+ readonly id?: string | undefined;
50
+ readonly kind: 'vocabulary';
51
+ readonly to: string;
52
+ };
53
+
54
+ const governedTransitionForPlan = (
55
+ plan: GovernedReceiptPlan,
56
+ transitionIds: readonly (string | undefined)[] = []
57
+ ) => {
420
58
  if (plan.kind !== 'vocabulary') {
421
59
  return;
422
60
  }
423
- const transitionIds = [
424
- plan.id,
425
- run.provenance?.transitionId,
426
- run.plan.transitionId,
427
- ];
428
- for (const transitionId of transitionIds) {
61
+ for (const transitionId of [plan.id, ...transitionIds]) {
429
62
  if (transitionId === undefined) {
430
63
  continue;
431
64
  }
@@ -443,50 +76,43 @@ const governedTransitionForRun = (run: z.infer<typeof historyRunSchema>) => {
443
76
  );
444
77
  };
445
78
 
446
- const loadHistoryFile = (rootDir: string, name: string): HistoryFileResult => {
447
- const absolutePath = join(rootDir, '.trails', 'regrade', 'history', name);
448
- const observedPath = normalizePath(relative(rootDir, absolutePath));
449
- let json: unknown;
450
- try {
451
- json = JSON.parse(readFileSync(absolutePath, 'utf8'));
452
- } catch {
453
- return historyIssue(
454
- observedPath,
455
- 'Committed Regrade history is not valid JSON.'
456
- );
457
- }
458
- const parsed = historyArtifactSchema.safeParse(json);
459
- if (!parsed.success) {
79
+ const loadReceiptHistoryFile = (
80
+ json: unknown,
81
+ observedPath: string
82
+ ): HistoryFileResult => {
83
+ const resolved = resolveReceiptForWarden(json);
84
+ if (resolved.value === undefined) {
460
85
  return historyIssue(
461
86
  observedPath,
462
- 'Committed Regrade history has an invalid evidence shape.'
87
+ `Committed Regrade receipt has invalid deterministic evidence (${resolved.error ?? 'unknown error'}).`
463
88
  );
464
89
  }
465
- if (normalizePath(parsed.data.path) !== observedPath) {
90
+ const { artifact, runs } = resolved.value;
91
+ if (normalizePath(artifact.path) !== observedPath) {
466
92
  return historyIssue(
467
93
  observedPath,
468
94
  'Committed Regrade history path does not match its observed file.'
469
95
  );
470
96
  }
471
-
472
- const latestRun = parsed.data.runs.at(-1);
97
+ const latest = runs.at(-1);
473
98
  const transition =
474
- latestRun === undefined ? undefined : governedTransitionForRun(latestRun);
99
+ latest === undefined
100
+ ? undefined
101
+ : governedTransitionForPlan(latest.plan, [artifact.id]);
475
102
  if (transition === undefined) {
476
103
  return { kind: 'ignore' };
477
104
  }
478
- const allRunsMatchTransition = parsed.data.runs.every((run) => {
479
- const runPlan = run.plan.plan;
105
+ const allRunsMatchTransition = runs.every(({ plan }) => {
480
106
  if (
481
- runPlan.kind !== 'vocabulary' ||
482
- governedTransitionForRun(run)?.id !== transition.id ||
483
- runPlan.from !== transition.from
107
+ plan.kind !== 'vocabulary' ||
108
+ governedTransitionForPlan(plan, [artifact.id])?.id !== transition.id ||
109
+ plan.from !== transition.from
484
110
  ) {
485
111
  return false;
486
112
  }
487
113
  return transition.target.kind === 'single'
488
- ? transition.target.to === runPlan.to
489
- : transition.target.options.some((option) => option.to === runPlan.to);
114
+ ? transition.target.to === plan.to
115
+ : transition.target.options.some((option) => option.to === plan.to);
490
116
  });
491
117
  if (!allRunsMatchTransition) {
492
118
  return historyIssue(
@@ -495,71 +121,34 @@ const loadHistoryFile = (rootDir: string, name: string): HistoryFileResult => {
495
121
  transition.id
496
122
  );
497
123
  }
498
- const governedRuns = parsed.data.runs.map((run) =>
499
- governedHistoryRunSchema.safeParse(run)
500
- );
501
- const rawRuns = (
502
- json as {
503
- readonly runs: readonly RawGovernedHistoryRun[];
504
- }
505
- ).runs;
506
- if (
507
- transition.provenance.mode === 'regrade-history' &&
508
- (governedRuns.some((run) => !run.success) ||
509
- governedRuns.some(
510
- (run, index) =>
511
- run.success &&
512
- !validatesDeterministicEvidence(
513
- run.data,
514
- rawRuns[index],
515
- transition.id
516
- )
517
- ))
518
- ) {
519
- return historyIssue(
520
- observedPath,
521
- 'Committed Regrade history has invalid deterministic run evidence.',
522
- transition.id
523
- );
524
- }
525
- if (
526
- parsed.data.runs.some(
527
- (run) =>
528
- run.provenance !== undefined &&
529
- run.provenance.transitionId !== transition.id
530
- )
531
- ) {
532
- return historyIssue(
533
- observedPath,
534
- 'Committed Regrade history provenance names a different governed transition.',
535
- transition.id
536
- );
537
- }
538
- if (
539
- transition.provenance.mode === 'regrade-history' &&
540
- parsed.data.runs.some((run) => run.provenance === undefined)
541
- ) {
542
- return historyIssue(
543
- observedPath,
544
- 'Committed Regrade history lacks required governed provenance.',
545
- transition.id
546
- );
547
- }
548
- const provenance = parsed.data.runs.at(-1)?.provenance;
549
- const facts = latestRunFacts(parsed.data.runs.at(-1));
550
124
  return {
551
125
  kind: 'evidence',
552
126
  value: {
553
- ...facts,
554
- id: parsed.data.id,
555
- path: parsed.data.path,
556
- ...(provenance === undefined ? {} : { provenance }),
557
- runCount: parsed.data.runs.length,
127
+ caseSensitive: latest?.caseSensitive ?? false,
128
+ id: artifact.id,
129
+ latestFormJudgments: latest?.forms ?? [],
130
+ path: artifact.path,
131
+ runCount: runs.length,
558
132
  transitionId: transition.id,
559
133
  },
560
134
  };
561
135
  };
562
136
 
137
+ const loadHistoryFile = (rootDir: string, name: string): HistoryFileResult => {
138
+ const absolutePath = join(rootDir, '.trails', 'regrade', 'history', name);
139
+ const observedPath = normalizePath(relative(rootDir, absolutePath));
140
+ let json: unknown;
141
+ try {
142
+ json = JSON.parse(readFileSync(absolutePath, 'utf8'));
143
+ } catch {
144
+ return historyIssue(
145
+ observedPath,
146
+ 'Committed Regrade history is not valid JSON.'
147
+ );
148
+ }
149
+ return loadReceiptHistoryFile(json, observedPath);
150
+ };
151
+
563
152
  export const loadGovernedVocabularyHistory = (
564
153
  rootDir: string
565
154
  ): GovernedVocabularyHistoryIndex => {