@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 +21 -0
- package/package.json +10 -10
- package/src/index.ts +1 -1
- package/src/regrade-history.ts +54 -465
- package/src/regrade-receipt.ts +484 -0
- package/src/rules/governed-symbol-residue.ts +4 -0
- package/src/rules/governed-vocabulary-permutation-watch.ts +20 -19
- package/src/rules/index.ts +1 -1
- package/src/rules/public-export-example-coverage.ts +25 -37
- package/src/rules/retired-vocabulary.ts +30 -0
- package/src/rules/types.ts +11 -8
- package/src/trails/schema.ts +11 -8
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.
|
|
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.
|
|
36
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
37
|
-
"@ontrails/config": "^1.0.0-beta.
|
|
38
|
-
"@ontrails/permits": "^1.0.0-beta.
|
|
39
|
-
"@ontrails/source": "^1.0.0-beta.
|
|
40
|
-
"@ontrails/store": "^1.0.0-beta.
|
|
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
|
+
"@ontrails/testing": "^1.0.0-beta.46"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
49
|
-
"@ontrails/topography": "^1.0.0-beta.
|
|
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
package/src/regrade-history.ts
CHANGED
|
@@ -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
|
-
|
|
419
|
-
|
|
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
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
87
|
+
`Committed Regrade receipt has invalid deterministic evidence (${resolved.error ?? 'unknown error'}).`
|
|
463
88
|
);
|
|
464
89
|
}
|
|
465
|
-
|
|
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
|
-
|
|
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 =
|
|
479
|
-
const runPlan = run.plan.plan;
|
|
105
|
+
const allRunsMatchTransition = runs.every(({ plan }) => {
|
|
480
106
|
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 ===
|
|
489
|
-
: transition.target.options.some((option) => option.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
|
-
|
|
554
|
-
id:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
runCount:
|
|
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 => {
|