@ontrails/trails 1.0.0-beta.41 → 1.0.0-beta.43
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 +57 -0
- package/README.md +1 -1
- package/package.json +16 -17
- package/src/mcp-options.ts +1 -0
- package/src/regrade/audit.ts +482 -0
- package/src/regrade/history.ts +250 -42
- package/src/regrade/live-api-preserve.ts +117 -2
- package/src/regrade/plan-artifact.ts +192 -13
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/source-transaction.ts +75 -0
- package/src/release/check.ts +312 -10
- package/src/release/config.ts +10 -0
- package/src/release/index.ts +5 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/run-completions-install.ts +4 -4
- package/src/run-trace.ts +4 -4
- package/src/trails/dev-support.ts +1 -1
- package/src/trails/regrade.ts +802 -125
- package/src/trails/release-check.ts +8 -0
- package/src/trails/run-example.ts +2 -2
- package/src/trails/survey.ts +6 -6
- package/src/trails/topo-output-schemas.ts +8 -8
- package/src/trails/topo-reports.ts +44 -44
- package/src/trails/topo-store-support.ts +2 -4
- package/src/trails/warden-guide.ts +1 -1
package/src/regrade/history.ts
CHANGED
|
@@ -9,6 +9,12 @@ import { InternalError, Result, ValidationError } from '@ontrails/core';
|
|
|
9
9
|
import type { Result as TrailsResult } from '@ontrails/core';
|
|
10
10
|
import { regradeReportOutput } from '@ontrails/regrade';
|
|
11
11
|
import type { RegradeReport } from '@ontrails/regrade';
|
|
12
|
+
import {
|
|
13
|
+
getGovernedVocabularyTransition,
|
|
14
|
+
governedVocabularyHistoryProvenanceSchema,
|
|
15
|
+
listGovernedVocabularyTransitions,
|
|
16
|
+
} from '@ontrails/warden';
|
|
17
|
+
import type { GovernedVocabularyHistoryProvenance } from '@ontrails/warden';
|
|
12
18
|
import { createHash } from 'node:crypto';
|
|
13
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
20
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
@@ -61,6 +67,7 @@ const regradeHistoryRunSchema = z
|
|
|
61
67
|
.string()
|
|
62
68
|
.min(1)
|
|
63
69
|
.describe('Canonical content hash of the resolved plan body'),
|
|
70
|
+
provenance: governedVocabularyHistoryProvenanceSchema.optional(),
|
|
64
71
|
report: regradeReportOutput.describe('Applied report recorded by the run'),
|
|
65
72
|
})
|
|
66
73
|
.strict();
|
|
@@ -85,6 +92,7 @@ interface RegradeHistoryRun {
|
|
|
85
92
|
readonly lockHashAtRun: string;
|
|
86
93
|
readonly plan: RegradePlanArtifact;
|
|
87
94
|
readonly planContentHash: string;
|
|
95
|
+
readonly provenance?: GovernedVocabularyHistoryProvenance;
|
|
88
96
|
readonly report: RegradeReport;
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -97,7 +105,9 @@ export interface RegradeHistoryArtifact {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
export interface RegradeHistorySummary {
|
|
108
|
+
readonly id: string;
|
|
100
109
|
readonly path: string;
|
|
110
|
+
readonly provenance?: GovernedVocabularyHistoryProvenance;
|
|
101
111
|
readonly schemaVersion: number;
|
|
102
112
|
readonly status: 'applied' | 'replay';
|
|
103
113
|
}
|
|
@@ -195,6 +205,117 @@ export const mintTransitionId = (
|
|
|
195
205
|
.digest('hex')
|
|
196
206
|
.slice(0, 12);
|
|
197
207
|
|
|
208
|
+
const targetIncludesPlanTarget = (
|
|
209
|
+
transition: NonNullable<ReturnType<typeof getGovernedVocabularyTransition>>,
|
|
210
|
+
to: string
|
|
211
|
+
): boolean =>
|
|
212
|
+
transition.target.kind === 'single'
|
|
213
|
+
? transition.target.to === to
|
|
214
|
+
: transition.target.options.some((option) => option.to === to);
|
|
215
|
+
|
|
216
|
+
const governedTransitionForPlan = (
|
|
217
|
+
plan: Extract<RegradePlanBody, { kind: 'vocabulary' }>
|
|
218
|
+
) => {
|
|
219
|
+
const idTransition =
|
|
220
|
+
plan.id === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: getGovernedVocabularyTransition(plan.id);
|
|
223
|
+
return (
|
|
224
|
+
idTransition ??
|
|
225
|
+
listGovernedVocabularyTransitions().find(
|
|
226
|
+
(transition) => transition.from === plan.from
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const validateGovernedRegradePlan = (
|
|
232
|
+
artifact: RegradePlanArtifact
|
|
233
|
+
): TrailsResult<void, ValidationError> => {
|
|
234
|
+
const { plan } = artifact;
|
|
235
|
+
if (plan.kind !== 'vocabulary') {
|
|
236
|
+
return Result.ok();
|
|
237
|
+
}
|
|
238
|
+
const transition = governedTransitionForPlan(plan);
|
|
239
|
+
if (transition === undefined) {
|
|
240
|
+
return Result.ok();
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
transition.from === plan.from &&
|
|
244
|
+
targetIncludesPlanTarget(transition, plan.to)
|
|
245
|
+
) {
|
|
246
|
+
return Result.ok();
|
|
247
|
+
}
|
|
248
|
+
return Result.err(
|
|
249
|
+
new ValidationError(
|
|
250
|
+
'Governed Regrade plan does not match its registry transition.',
|
|
251
|
+
{
|
|
252
|
+
context: {
|
|
253
|
+
plan: { from: plan.from, id: plan.id, to: plan.to },
|
|
254
|
+
registry: { from: transition.from, id: transition.id },
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const governedProvenanceForRun = (params: {
|
|
262
|
+
readonly artifact: RegradePlanArtifact;
|
|
263
|
+
readonly completionReport: RegradeReport;
|
|
264
|
+
readonly planContentHash: string;
|
|
265
|
+
readonly report: RegradeReport;
|
|
266
|
+
}): TrailsResult<
|
|
267
|
+
GovernedVocabularyHistoryProvenance | undefined,
|
|
268
|
+
ValidationError
|
|
269
|
+
> => {
|
|
270
|
+
const { plan } = params.artifact;
|
|
271
|
+
const validation = validateGovernedRegradePlan(params.artifact);
|
|
272
|
+
if (validation.isErr()) {
|
|
273
|
+
return validation;
|
|
274
|
+
}
|
|
275
|
+
if (plan.kind !== 'vocabulary') {
|
|
276
|
+
return Result.ok();
|
|
277
|
+
}
|
|
278
|
+
const transition = governedTransitionForPlan(plan);
|
|
279
|
+
if (transition === undefined) {
|
|
280
|
+
return Result.ok();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const reviewPending = params.report.review;
|
|
284
|
+
return Result.ok({
|
|
285
|
+
disposition: reviewPending > 0 ? 'review-follow-up' : 'applied-clean',
|
|
286
|
+
kind: 'governed-vocabulary',
|
|
287
|
+
planContentHash: params.planContentHash,
|
|
288
|
+
reviewPending,
|
|
289
|
+
safeApplied: params.report.apply?.applied ?? params.report.rewritten,
|
|
290
|
+
sourceHashAfter: regradeSourceHash(params.completionReport),
|
|
291
|
+
sourceHashBefore: regradeSourceHash(params.report),
|
|
292
|
+
transitionId: transition.id,
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const governedProvenanceMatchesRun = (params: {
|
|
297
|
+
readonly expected: GovernedVocabularyHistoryProvenance | undefined;
|
|
298
|
+
readonly run: RegradeHistoryRun;
|
|
299
|
+
}): boolean => {
|
|
300
|
+
const { expected, run } = params;
|
|
301
|
+
const actual = run.provenance;
|
|
302
|
+
if (actual === undefined || expected === undefined) {
|
|
303
|
+
return actual === expected;
|
|
304
|
+
}
|
|
305
|
+
return (
|
|
306
|
+
actual.disposition === expected.disposition &&
|
|
307
|
+
actual.kind === expected.kind &&
|
|
308
|
+
actual.planContentHash === expected.planContentHash &&
|
|
309
|
+
actual.reviewPending === expected.reviewPending &&
|
|
310
|
+
actual.safeApplied === expected.safeApplied &&
|
|
311
|
+
(regradeSourceHashMatches(actual.sourceHashBefore, run.report) ||
|
|
312
|
+
run[rawReportHash] === actual.sourceHashBefore) &&
|
|
313
|
+
(regradeSourceHashMatches(actual.sourceHashAfter, run.completionReport) ||
|
|
314
|
+
run[rawCompletionReportHash] === actual.sourceHashAfter) &&
|
|
315
|
+
actual.transitionId === expected.transitionId
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
198
319
|
/**
|
|
199
320
|
* Verify every recorded run at its own stamped lock: recompute the plan
|
|
200
321
|
* content hash and lock hash from the recorded plan and report, then compare
|
|
@@ -242,10 +363,121 @@ export const verifyRegradeHistoryRuns = (
|
|
|
242
363
|
})
|
|
243
364
|
);
|
|
244
365
|
}
|
|
366
|
+
const expectedProvenance = governedProvenanceForRun({
|
|
367
|
+
artifact: run.plan,
|
|
368
|
+
completionReport: run.completionReport,
|
|
369
|
+
planContentHash: run.planContentHash,
|
|
370
|
+
report: run.report,
|
|
371
|
+
});
|
|
372
|
+
if (expectedProvenance.isErr()) {
|
|
373
|
+
return expectedProvenance;
|
|
374
|
+
}
|
|
375
|
+
const transition =
|
|
376
|
+
run.plan.plan.kind === 'vocabulary'
|
|
377
|
+
? governedTransitionForPlan(run.plan.plan)
|
|
378
|
+
: undefined;
|
|
379
|
+
if (
|
|
380
|
+
transition?.provenance.mode === 'regrade-history' &&
|
|
381
|
+
run.provenance === undefined
|
|
382
|
+
) {
|
|
383
|
+
return Result.err(
|
|
384
|
+
new ValidationError('Regrade history run lacks governed provenance.', {
|
|
385
|
+
context: { path: artifact.path, run: index },
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
run.provenance !== undefined &&
|
|
391
|
+
!governedProvenanceMatchesRun({
|
|
392
|
+
expected: expectedProvenance.value,
|
|
393
|
+
run,
|
|
394
|
+
})
|
|
395
|
+
) {
|
|
396
|
+
return Result.err(
|
|
397
|
+
new ValidationError('Regrade history run provenance mismatch.', {
|
|
398
|
+
context: { path: artifact.path, run: index },
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|
|
245
402
|
}
|
|
246
403
|
return Result.ok({ runs: artifact.runs.length });
|
|
247
404
|
};
|
|
248
405
|
|
|
406
|
+
const historyEntryFor = (params: {
|
|
407
|
+
readonly artifact: RegradePlanArtifact;
|
|
408
|
+
readonly completionReport: RegradeReport;
|
|
409
|
+
readonly lockHashAtRun: string;
|
|
410
|
+
readonly planContentHash: string;
|
|
411
|
+
readonly report: RegradeReport;
|
|
412
|
+
}): TrailsResult<RegradeHistoryRun, ValidationError> => {
|
|
413
|
+
const provenance = governedProvenanceForRun(params);
|
|
414
|
+
if (provenance.isErr()) {
|
|
415
|
+
return provenance;
|
|
416
|
+
}
|
|
417
|
+
return Result.ok({
|
|
418
|
+
completionReport: params.completionReport,
|
|
419
|
+
completionReportHash: regradeSourceHash(params.completionReport),
|
|
420
|
+
lockHashAtRun: params.lockHashAtRun,
|
|
421
|
+
plan: params.artifact,
|
|
422
|
+
planContentHash: params.planContentHash,
|
|
423
|
+
...(provenance.value === undefined ? {} : { provenance: provenance.value }),
|
|
424
|
+
report: params.report,
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const historySummaryFor = (
|
|
429
|
+
artifact: Pick<RegradeHistoryArtifact, 'id' | 'path' | 'schemaVersion'>,
|
|
430
|
+
status: RegradeHistorySummary['status'],
|
|
431
|
+
provenance?: GovernedVocabularyHistoryProvenance
|
|
432
|
+
): RegradeHistorySummary => ({
|
|
433
|
+
id: artifact.id,
|
|
434
|
+
path: artifact.path,
|
|
435
|
+
...(provenance === undefined ? {} : { provenance }),
|
|
436
|
+
schemaVersion: artifact.schemaVersion,
|
|
437
|
+
status,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const writableHistoryArtifact = (artifact: RegradeHistoryArtifact) => ({
|
|
441
|
+
id: artifact.id,
|
|
442
|
+
kind: artifact.kind,
|
|
443
|
+
path: artifact.path,
|
|
444
|
+
runs: artifact.runs.map((run) => ({
|
|
445
|
+
completionReport: run[rawCompletionReport] ?? run.completionReport,
|
|
446
|
+
completionReportHash: run.completionReportHash,
|
|
447
|
+
lockHashAtRun: run.lockHashAtRun,
|
|
448
|
+
plan: run.plan,
|
|
449
|
+
planContentHash: run.planContentHash,
|
|
450
|
+
...(run.provenance === undefined ? {} : { provenance: run.provenance }),
|
|
451
|
+
report: run[rawReport] ?? run.report,
|
|
452
|
+
})),
|
|
453
|
+
schemaVersion: artifact.schemaVersion,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const nextHistoryArtifact = (params: {
|
|
457
|
+
readonly entry: RegradeHistoryRun;
|
|
458
|
+
readonly lockHashAtRun: string;
|
|
459
|
+
readonly path: string;
|
|
460
|
+
readonly plan: RegradePlanArtifact;
|
|
461
|
+
readonly planContentHash: string;
|
|
462
|
+
readonly prior: RegradeHistoryArtifact | undefined;
|
|
463
|
+
}): RegradeHistoryArtifact => ({
|
|
464
|
+
id:
|
|
465
|
+
params.prior?.id ??
|
|
466
|
+
params.plan.transitionId ??
|
|
467
|
+
mintTransitionId(
|
|
468
|
+
regradePlanSlugForBody(params.plan.plan),
|
|
469
|
+
params.planContentHash,
|
|
470
|
+
params.lockHashAtRun
|
|
471
|
+
),
|
|
472
|
+
kind: 'regrade-history',
|
|
473
|
+
path: params.path,
|
|
474
|
+
runs:
|
|
475
|
+
params.prior === undefined
|
|
476
|
+
? [params.entry]
|
|
477
|
+
: [...params.prior.runs, params.entry],
|
|
478
|
+
schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
|
|
479
|
+
});
|
|
480
|
+
|
|
249
481
|
/**
|
|
250
482
|
* Append one applied run to the transition's consolidated history file. A
|
|
251
483
|
* run whose plan content hash and source evidence equal either the last run's
|
|
@@ -267,14 +499,16 @@ export const appendRegradeHistoryRun = (params: {
|
|
|
267
499
|
const lockHashAtRun = regradeSourceHash(params.report);
|
|
268
500
|
const currentSourceHashes = regradeSourceHashes(params.report);
|
|
269
501
|
const completionReport = params.completedReport ?? params.report;
|
|
270
|
-
const entry
|
|
502
|
+
const entry = historyEntryFor({
|
|
503
|
+
artifact: params.artifact,
|
|
271
504
|
completionReport,
|
|
272
|
-
completionReportHash: regradeSourceHash(completionReport),
|
|
273
505
|
lockHashAtRun,
|
|
274
|
-
plan: params.artifact,
|
|
275
506
|
planContentHash,
|
|
276
507
|
report: params.report,
|
|
277
|
-
};
|
|
508
|
+
});
|
|
509
|
+
if (entry.isErr()) {
|
|
510
|
+
return entry;
|
|
511
|
+
}
|
|
278
512
|
|
|
279
513
|
let prior: RegradeHistoryArtifact | undefined;
|
|
280
514
|
if (existsSync(absolutePath)) {
|
|
@@ -334,43 +568,19 @@ export const appendRegradeHistoryRun = (params: {
|
|
|
334
568
|
(currentSourceHashes.includes(lastRun.lockHashAtRun) ||
|
|
335
569
|
currentSourceHashes.includes(lastRun.completionReportHash))
|
|
336
570
|
) {
|
|
337
|
-
return Result.ok(
|
|
338
|
-
path: relativePath,
|
|
339
|
-
schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
|
|
340
|
-
status: 'replay',
|
|
341
|
-
});
|
|
571
|
+
return Result.ok(historySummaryFor(prior, 'replay', lastRun.provenance));
|
|
342
572
|
}
|
|
343
573
|
}
|
|
344
574
|
|
|
345
|
-
const artifact
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
? (params.artifact.transitionId ??
|
|
349
|
-
mintTransitionId(
|
|
350
|
-
regradePlanSlugForBody(params.artifact.plan),
|
|
351
|
-
planContentHash,
|
|
352
|
-
lockHashAtRun
|
|
353
|
-
))
|
|
354
|
-
: prior.id,
|
|
355
|
-
kind: 'regrade-history',
|
|
575
|
+
const artifact = nextHistoryArtifact({
|
|
576
|
+
entry: entry.value,
|
|
577
|
+
lockHashAtRun,
|
|
356
578
|
path: relativePath,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
kind: artifact.kind,
|
|
363
|
-
path: artifact.path,
|
|
364
|
-
runs: artifact.runs.map((run) => ({
|
|
365
|
-
completionReport: run[rawCompletionReport] ?? run.completionReport,
|
|
366
|
-
completionReportHash: run.completionReportHash,
|
|
367
|
-
lockHashAtRun: run.lockHashAtRun,
|
|
368
|
-
plan: run.plan,
|
|
369
|
-
planContentHash: run.planContentHash,
|
|
370
|
-
report: run[rawReport] ?? run.report,
|
|
371
|
-
})),
|
|
372
|
-
schemaVersion: artifact.schemaVersion,
|
|
373
|
-
};
|
|
579
|
+
plan: params.artifact,
|
|
580
|
+
planContentHash,
|
|
581
|
+
prior,
|
|
582
|
+
});
|
|
583
|
+
const writableArtifact = writableHistoryArtifact(artifact);
|
|
374
584
|
const parsed = regradeHistoryArtifactSchema.safeParse(writableArtifact);
|
|
375
585
|
if (!parsed.success) {
|
|
376
586
|
return Result.err(
|
|
@@ -393,11 +603,9 @@ export const appendRegradeHistoryRun = (params: {
|
|
|
393
603
|
})
|
|
394
604
|
);
|
|
395
605
|
}
|
|
396
|
-
return Result.ok(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
status: 'applied',
|
|
400
|
-
});
|
|
606
|
+
return Result.ok(
|
|
607
|
+
historySummaryFor(artifact, 'applied', entry.value.provenance)
|
|
608
|
+
);
|
|
401
609
|
};
|
|
402
610
|
|
|
403
611
|
const hasPathSeparator = (value: string): boolean =>
|
|
@@ -1,8 +1,123 @@
|
|
|
1
|
+
import { Result, ValidationError, escapeRegExp } from '@ontrails/core';
|
|
2
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
1
3
|
import type {
|
|
2
4
|
VocabularyPreserveInventoryEntry,
|
|
3
5
|
VocabularyRegradePlan,
|
|
4
6
|
} from '@ontrails/regrade';
|
|
7
|
+
import { deriveVocabularyFormProposals } from '@ontrails/regrade';
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
const lockShapeSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
topoGraph: z
|
|
15
|
+
.object({
|
|
16
|
+
entries: z.array(z.object({ id: z.string() }).passthrough()),
|
|
17
|
+
library: z
|
|
18
|
+
.object({
|
|
19
|
+
exports: z.array(
|
|
20
|
+
z.object({ exportName: z.string() }).passthrough()
|
|
21
|
+
),
|
|
22
|
+
})
|
|
23
|
+
.passthrough()
|
|
24
|
+
.optional(),
|
|
25
|
+
})
|
|
26
|
+
.passthrough(),
|
|
27
|
+
})
|
|
28
|
+
.passthrough();
|
|
29
|
+
|
|
30
|
+
type LockShape = z.output<typeof lockShapeSchema>;
|
|
31
|
+
|
|
32
|
+
const liveApiValues = (
|
|
33
|
+
lock: LockShape
|
|
34
|
+
): readonly {
|
|
35
|
+
readonly evidence: string;
|
|
36
|
+
readonly value: string;
|
|
37
|
+
}[] => {
|
|
38
|
+
const values = new Map<string, string>();
|
|
39
|
+
for (const entry of lock.topoGraph?.entries ?? []) {
|
|
40
|
+
if (typeof entry.id === 'string') {
|
|
41
|
+
values.set(entry.id, `topo.entry:${entry.id}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const item of lock.topoGraph?.library?.exports ?? []) {
|
|
45
|
+
if (typeof item.exportName === 'string') {
|
|
46
|
+
values.set(item.exportName, `topo.library:${item.exportName}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...values.entries()]
|
|
50
|
+
.map(([value, evidence]) => ({ evidence, value }))
|
|
51
|
+
.toSorted((left, right) => left.value.localeCompare(right.value));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const identifierSegments = (value: string): readonly string[] =>
|
|
55
|
+
value
|
|
56
|
+
.split(/[^A-Za-z0-9]+/u)
|
|
57
|
+
.flatMap(
|
|
58
|
+
(part) => part.match(/[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+/gu) ?? []
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const liveApiValueContainsSource = (value: string, source: string): boolean => {
|
|
62
|
+
if (/^[A-Za-z][A-Za-z0-9]*$/u.test(source)) {
|
|
63
|
+
const normalized = source.toLowerCase();
|
|
64
|
+
return identifierSegments(value).some(
|
|
65
|
+
(segment) => segment.toLowerCase() === normalized
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return new RegExp(
|
|
69
|
+
`(?<![A-Za-z0-9_$])${escapeRegExp(source)}(?![A-Za-z0-9_$])`,
|
|
70
|
+
'iu'
|
|
71
|
+
).test(value);
|
|
72
|
+
};
|
|
5
73
|
|
|
6
74
|
export const deriveLiveApiPreserveInventory = async (
|
|
7
|
-
|
|
8
|
-
|
|
75
|
+
plan: VocabularyRegradePlan,
|
|
76
|
+
rootDir = process.cwd()
|
|
77
|
+
): Promise<
|
|
78
|
+
TrailsResult<readonly VocabularyPreserveInventoryEntry[], ValidationError>
|
|
79
|
+
> => {
|
|
80
|
+
const lockPath = join(rootDir, 'trails.lock');
|
|
81
|
+
if (!existsSync(lockPath)) {
|
|
82
|
+
return Result.ok([]);
|
|
83
|
+
}
|
|
84
|
+
let input: unknown;
|
|
85
|
+
try {
|
|
86
|
+
input = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return Result.err(
|
|
89
|
+
new ValidationError(
|
|
90
|
+
'Unable to parse trails.lock for live API preserves.',
|
|
91
|
+
{
|
|
92
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
93
|
+
context: { path: lockPath },
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const parsed = lockShapeSchema.safeParse(input);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
return Result.err(
|
|
101
|
+
new ValidationError(
|
|
102
|
+
'trails.lock does not contain a compatible TopoGraph for live API preserves.',
|
|
103
|
+
{ context: { issues: parsed.error.issues, path: lockPath } }
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const sourceForms = deriveVocabularyFormProposals(plan).map(
|
|
108
|
+
(proposal) => proposal.from
|
|
109
|
+
);
|
|
110
|
+
return Result.ok(
|
|
111
|
+
liveApiValues(parsed.data)
|
|
112
|
+
.filter(({ value }) =>
|
|
113
|
+
sourceForms.some((source) => liveApiValueContainsSource(value, source))
|
|
114
|
+
)
|
|
115
|
+
.map(({ evidence, value }) => ({
|
|
116
|
+
disposition: 'preserve-current-live-api' as const,
|
|
117
|
+
evidence: [evidence],
|
|
118
|
+
pattern: escapeRegExp(value),
|
|
119
|
+
reason: 'current-live-topo-api',
|
|
120
|
+
source: 'derived-live-api' as const,
|
|
121
|
+
}))
|
|
122
|
+
);
|
|
123
|
+
};
|