@ontrails/trails 1.0.0-beta.42 → 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 +55 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# trails
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.43
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`4fb20a6`](https://github.com/outfitter-dev/trails/commit/4fb20a68e1ed98972d99fed8b2df96bfa6804bd3): Derive deterministic, provenance-bearing vocabulary plan proposals from a minimal `from`/`to` seed, including morphology, public and compound identifier review, filename and reference-closure candidates, namespace census, and validated live-topo API preserves. Classified governed transitions retain their registry identity while routing every governed form to review instead of inventing a single safe successor.
|
|
8
|
+
- [`d3215b0`](https://github.com/outfitter-dev/trails/commit/d3215b0966f13af2a72ece9adbcc2c68c70f81b6): Add governed file renames to vocabulary Regrade plans, with move-first derived reference closure and persisted policy-aware evidence across CLI and MCP.
|
|
9
|
+
- [`5a7d22a`](https://github.com/outfitter-dev/trails/commit/5a7d22ab9d674f86b28758f23c3e94a17efb5be1): Add three-tier vocabulary migration scope. Protected historical paths remain
|
|
10
|
+
scanned and counted as `historical-by-policy` without being rewritten, while
|
|
11
|
+
reports expose scope-tier totals and census-expected teaching-surface coverage
|
|
12
|
+
through equivalent CLI and MCP plan schemas. Applied vocabulary occurrences
|
|
13
|
+
remain auditable in history with an `applied` verdict, while policy-only
|
|
14
|
+
evidence does not make an active plan stale merely by being recorded.
|
|
15
|
+
- [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
|
|
16
|
+
to derive/derived for contract-owned fact production and render/rendered for
|
|
17
|
+
surface presentation. Public type, helper, rule, relation, and report names move
|
|
18
|
+
without compatibility aliases; ordinary repository/project nouns remain
|
|
19
|
+
explicit preserves or structured review inventory.
|
|
20
|
+
- [`9f0842e`](https://github.com/outfitter-dev/trails/commit/9f0842ee9d7c7155d86a4fd023760ac0a5636f5d): Retire the temporary root vocabulary-cutover toolchain now that Regrade owns
|
|
21
|
+
structured migration plans, safe rewrites, classification, census, CLI/MCP
|
|
22
|
+
reports, and immutable history. Remove the obsolete source exemptions so
|
|
23
|
+
Oxlint and Warden enforce the durable transition contract directly, and add a
|
|
24
|
+
history-driven Regrade audit surface for current-tree regression checks.
|
|
25
|
+
- [`bd1bd96`](https://github.com/outfitter-dev/trails/commit/bd1bd96b90cd8b55f73061e4078a14cd75bed745): Require committed Regrade provenance for governed vocabulary transitions.
|
|
26
|
+
|
|
27
|
+
Applied governed plans now expose deterministic transition, plan, source,
|
|
28
|
+
safe-apply, and review-follow-up evidence through history results. Warden loads
|
|
29
|
+
committed history into project context, cites it for reintroduced symbols, and
|
|
30
|
+
rejects invalid or missing provenance for transitions that require Regrade in
|
|
31
|
+
the workspace that owns the governed registry, without making downstream apps
|
|
32
|
+
prove the framework's own migrations.
|
|
33
|
+
Portable validation accepts the authoritative numeric file-rename counters
|
|
34
|
+
persisted by Regrade history.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- [`742387e`](https://github.com/outfitter-dev/trails/commit/742387ea27e1f9fb9490147b871ae8c5a59903ae): Allow release checks to recognize changeset edits that only remove release rows
|
|
39
|
+
for packages absent from the live workspace while rejecting any additional
|
|
40
|
+
changes hidden beside that cleanup.
|
|
41
|
+
- [`18a14e2`](https://github.com/outfitter-dev/trails/commit/18a14e220728be7108f3fe2864455c52305aef9f): Finish the v1 vocabulary reset cleanup by retaining the facet guard, adding a
|
|
42
|
+
durable TopoGraph artifact-family guard, teaching the live lexicon directly in
|
|
43
|
+
agent guidance, and replacing completed reset-family placeholders in public
|
|
44
|
+
Regrade examples and CLI schema help.
|
|
45
|
+
- [`113aed6`](https://github.com/outfitter-dev/trails/commit/113aed62d20041e35b0cf9d6c1b1a18df4b88f57): Rename the dependency-light observability owner from `@ontrails/observe` to
|
|
46
|
+
`@ontrails/observability` as a pre-v1 hard cut. Update dependent packages,
|
|
47
|
+
documentation, package discovery, and the governed Regrade route; no
|
|
48
|
+
compatibility package or old import route is retained.
|
|
49
|
+
- [`0938e7b`](https://github.com/outfitter-dev/trails/commit/0938e7badc0c5470d194139d642b673658d099e0): Fold the removed `@ontrails/tracing` package into the truthful existing
|
|
50
|
+
owners: intrinsic trace contracts remain in core, developer-state tooling now
|
|
51
|
+
lives at `@ontrails/observability/dev`, and the dependency-light OTel adapter
|
|
52
|
+
lives at `@ontrails/observability/otel`. There is intentionally no root-package
|
|
53
|
+
compatibility redirect because the former root had more than one owner.
|
|
54
|
+
- [`504881d`](https://github.com/outfitter-dev/trails/commit/504881df0d15ac92c3234be068c9e1930e1ffddf): Require release checks to prove an exact governed Regrade route, or an explicit
|
|
55
|
+
classified multi-owner fold, whenever a public `@ontrails/*` package disappears
|
|
56
|
+
from the publishable workspace inventory.
|
|
57
|
+
|
|
3
58
|
## 1.0.0-beta.42
|
|
4
59
|
|
|
5
60
|
## 1.0.0-beta.41
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Common workflows:
|
|
|
18
18
|
- `trails wayfind`, `trails wayfind --trails --intent read`, `trails wayfind <id> --contract`, `trails wayfind <id> --deps`, `trails wayfind <id> --impact`, `trails wayfind pattern "wayfind.*"`, `trails wayfind query "release drift"`, and `trails wayfind diff ...` read graph artifacts through Wayfinder. `trails wayfind file <file> --outline` is the operator-owned live-source exception, assembled with `@ontrails/source` and enriched with saved graph context when available.
|
|
19
19
|
- `trails schema <command...>` shows accepted CLI routes, aliases, flags, and schemas for an operator command or command namespace.
|
|
20
20
|
- `trails warden` runs Trails governance checks for contract and architecture drift. Use `--scope-exclude <glob>` or project `warden.scope.exclude` config when local notes, scratch space, or generated state should not be governed by Warden.
|
|
21
|
-
- `trails regrade plan <from> <to> --root-dir <path> --json` writes an active Regrade plan, `trails regrade plan --expand` stages wide-net review candidates in that plan, `trails regrade preview` reruns it without writing, and `trails regrade apply` consumes the plan and writes history. Use `plan --expand --dry-run` to inspect
|
|
21
|
+
- `trails regrade plan <from> <to> --root-dir <path> --json` writes an active Regrade plan, `trails regrade plan --expand` stages wide-net review candidates in that plan, `trails regrade preview` reruns it without writing, and `trails regrade apply` consumes the plan and writes history. The minimal `from`/`to` seed is the primary workflow: planning deterministically derives morphology proposals, source-observed naming and public-identifier review candidates, review-only filename moves with reference-closure evidence, a namespace census, and current live-topo API preserves into the artifact's `derivation` section. Each derived item records provenance; uncertain forms and file moves remain review inventory until explicitly authored into the plan. A malformed or incompatible `trails.lock` fails planning instead of silently dropping live-API preserves. Use `plan --expand --dry-run` to inspect additional observed candidates without writing the active plan, and add `--dry-run` to `apply` to prove the apply path without mutating source. Use `trails regrade plans` and `trails regrade check` when a workspace has active plans that need inspection before apply. MCP exposes the same artifact and lifecycle as `trails_plan_regrade`, `trails_list_regrades`, `trails_check_regrade`, `trails_preview_regrade`, and `trails_apply_regrade`.
|
|
22
22
|
- `trails guide` remains available for compatibility; prefer `trails wayfind --source live --module <app-module>` or saved-artifact Wayfinder reads for agent navigation.
|
|
23
23
|
|
|
24
24
|
Trails is contract-first: define trails once with typed input, Result output, examples, and meta; the framework derives CLI, MCP, HTTP, and future surfaces from the same contracts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/trails",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.43",
|
|
4
4
|
"bin": {
|
|
5
5
|
"trails": "./bin/trails.ts"
|
|
6
6
|
},
|
|
@@ -27,26 +27,25 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@clack/prompts": "^1.1.0",
|
|
30
|
-
"@ontrails/adapter-kit": "^1.0.0-beta.
|
|
31
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
32
|
-
"@ontrails/commander": "^1.0.0-beta.
|
|
33
|
-
"@ontrails/config": "^1.0.0-beta.
|
|
34
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
35
|
-
"@ontrails/http": "^1.0.0-beta.
|
|
36
|
-
"@ontrails/mcp": "^1.0.0-beta.
|
|
37
|
-
"@ontrails/
|
|
38
|
-
"@ontrails/permits": "^1.0.0-beta.
|
|
39
|
-
"@ontrails/regrade": "^1.0.0-beta.
|
|
40
|
-
"@ontrails/source": "^1.0.0-beta.
|
|
41
|
-
"@ontrails/topography": "^1.0.0-beta.
|
|
42
|
-
"@ontrails/
|
|
43
|
-
"@ontrails/warden": "^1.0.0-beta.42",
|
|
30
|
+
"@ontrails/adapter-kit": "^1.0.0-beta.43",
|
|
31
|
+
"@ontrails/cli": "^1.0.0-beta.43",
|
|
32
|
+
"@ontrails/commander": "^1.0.0-beta.43",
|
|
33
|
+
"@ontrails/config": "^1.0.0-beta.43",
|
|
34
|
+
"@ontrails/core": "^1.0.0-beta.43",
|
|
35
|
+
"@ontrails/http": "^1.0.0-beta.43",
|
|
36
|
+
"@ontrails/mcp": "^1.0.0-beta.43",
|
|
37
|
+
"@ontrails/observability": "^1.0.0-beta.43",
|
|
38
|
+
"@ontrails/permits": "^1.0.0-beta.43",
|
|
39
|
+
"@ontrails/regrade": "^1.0.0-beta.43",
|
|
40
|
+
"@ontrails/source": "^1.0.0-beta.43",
|
|
41
|
+
"@ontrails/topography": "^1.0.0-beta.43",
|
|
42
|
+
"@ontrails/warden": "^1.0.0-beta.43",
|
|
44
43
|
"commander": "^14.0.3",
|
|
45
44
|
"typescript": "^5.9.3",
|
|
46
45
|
"zod": "^4.3.5"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@ontrails/cloudflare": "^1.0.0-beta.
|
|
50
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
48
|
+
"@ontrails/cloudflare": "^1.0.0-beta.43",
|
|
49
|
+
"@ontrails/testing": "^1.0.0-beta.43"
|
|
51
50
|
}
|
|
52
51
|
}
|
package/src/mcp-options.ts
CHANGED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InternalError,
|
|
3
|
+
isPlainObject,
|
|
4
|
+
matchesAnyPathGlob,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
Result,
|
|
7
|
+
ValidationError,
|
|
8
|
+
} from '@ontrails/core';
|
|
9
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
10
|
+
import {
|
|
11
|
+
listVocabularyRegradeAuditPlansFromRegistry,
|
|
12
|
+
regradeReportOutput,
|
|
13
|
+
runVocabularyRegrade,
|
|
14
|
+
vocabularyDispositionValues,
|
|
15
|
+
vocabularyRegradePlanForInput,
|
|
16
|
+
} from '@ontrails/regrade';
|
|
17
|
+
import type { VocabularyRegradePlan } from '@ontrails/regrade';
|
|
18
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { basename, extname, join } from 'node:path';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
import { deriveLiveApiPreserveInventory } from './live-api-preserve.js';
|
|
23
|
+
import {
|
|
24
|
+
regradePlanArtifactSchema,
|
|
25
|
+
rootRelativePath,
|
|
26
|
+
} from './plan-artifact.js';
|
|
27
|
+
|
|
28
|
+
export const regradeAuditInputSchema = z.object({
|
|
29
|
+
failOnOpen: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe('Fail when a completed transition has current-tree residue'),
|
|
33
|
+
includeEntries: z
|
|
34
|
+
.enum(['actionable', 'all'])
|
|
35
|
+
.default('actionable')
|
|
36
|
+
.describe('Report entry detail to include for each audited transition'),
|
|
37
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
38
|
+
transitionIds: z
|
|
39
|
+
.array(z.string().min(1))
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Governed transition ids to audit; defaults to applied history'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const regradeAuditTransitionSchema = z.object({
|
|
45
|
+
report: z
|
|
46
|
+
.object({
|
|
47
|
+
dispositions: z
|
|
48
|
+
.partialRecord(
|
|
49
|
+
z.enum(vocabularyDispositionValues),
|
|
50
|
+
z.number().int().nonnegative()
|
|
51
|
+
)
|
|
52
|
+
.describe('Current Markdown occurrence counts by classification'),
|
|
53
|
+
entries: regradeReportOutput.shape.entries
|
|
54
|
+
.readonly()
|
|
55
|
+
.describe('Actionable file-level residue details'),
|
|
56
|
+
occurrences: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.nonnegative()
|
|
60
|
+
.describe('Total classified Markdown occurrences'),
|
|
61
|
+
open: z
|
|
62
|
+
.number()
|
|
63
|
+
.int()
|
|
64
|
+
.nonnegative()
|
|
65
|
+
.describe('Unresolved Markdown occurrences'),
|
|
66
|
+
scanned: z.number().int().nonnegative().describe('Files scanned'),
|
|
67
|
+
status: z.enum(['green', 'open']).describe('Transition audit status'),
|
|
68
|
+
})
|
|
69
|
+
.describe('Concise current-tree audit of the latest applied plan'),
|
|
70
|
+
source: z.string().describe('Committed Regrade history artifact'),
|
|
71
|
+
transitionId: z.string().describe('Stable Regrade transition identity'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const regradeAuditOutputSchema = z.object({
|
|
75
|
+
gate: z.object({
|
|
76
|
+
open: z
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.nonnegative()
|
|
80
|
+
.describe('Current-tree unresolved occurrence count'),
|
|
81
|
+
status: z
|
|
82
|
+
.enum(['green', 'open'])
|
|
83
|
+
.describe('Whether every audited transition remains complete'),
|
|
84
|
+
}),
|
|
85
|
+
transitions: z
|
|
86
|
+
.array(regradeAuditTransitionSchema)
|
|
87
|
+
.describe('Applied vocabulary transitions audited against current source'),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const regradeAuditHistoryPlanSchema = z.object({
|
|
91
|
+
id: z.string().min(1),
|
|
92
|
+
runs: z.array(z.object({ plan: regradePlanArtifactSchema })).min(1),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type RegradeAuditInput = z.output<typeof regradeAuditInputSchema>;
|
|
96
|
+
export type RegradeAuditOutput = z.output<typeof regradeAuditOutputSchema>;
|
|
97
|
+
|
|
98
|
+
interface RegradeAuditCandidate {
|
|
99
|
+
readonly plan: VocabularyRegradePlan;
|
|
100
|
+
readonly source: string;
|
|
101
|
+
readonly transitionId: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const historyTransitionId = (rawHistory: unknown): string | undefined => {
|
|
105
|
+
if (!isPlainObject(rawHistory)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const { id, runs } = rawHistory;
|
|
109
|
+
const latestRun = Array.isArray(runs) ? runs.at(-1) : undefined;
|
|
110
|
+
const planArtifact = isPlainObject(latestRun) ? latestRun['plan'] : undefined;
|
|
111
|
+
const plan = isPlainObject(planArtifact) ? planArtifact['plan'] : undefined;
|
|
112
|
+
const planId = isPlainObject(plan) ? plan['id'] : undefined;
|
|
113
|
+
if (typeof planId === 'string') {
|
|
114
|
+
return planId;
|
|
115
|
+
}
|
|
116
|
+
return typeof id === 'string' ? id : undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const selectedHistoryFileNamesFor = (
|
|
120
|
+
selectedTransitionIds: ReadonlySet<string> | null,
|
|
121
|
+
registryPlansById: ReadonlyMap<string, VocabularyRegradePlan>
|
|
122
|
+
): ReadonlySet<string> =>
|
|
123
|
+
new Set(
|
|
124
|
+
selectedTransitionIds === null
|
|
125
|
+
? []
|
|
126
|
+
: [...selectedTransitionIds].flatMap((transitionId) => {
|
|
127
|
+
const plan = registryPlansById.get(transitionId);
|
|
128
|
+
return plan === undefined ? [] : [`${plan.from}-to-${plan.to}.json`];
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const historyIsSelected = (
|
|
133
|
+
rawHistory: unknown,
|
|
134
|
+
historyFile: string,
|
|
135
|
+
selectedTransitionIds: ReadonlySet<string> | null,
|
|
136
|
+
selectedHistoryFileNames: ReadonlySet<string>
|
|
137
|
+
): boolean =>
|
|
138
|
+
selectedTransitionIds === null ||
|
|
139
|
+
selectedTransitionIds.has(historyTransitionId(rawHistory) ?? '') ||
|
|
140
|
+
selectedHistoryFileNames.has(basename(historyFile));
|
|
141
|
+
|
|
142
|
+
export const mergeAuditPlan = (
|
|
143
|
+
latestPlan: VocabularyRegradePlan,
|
|
144
|
+
registryPlan: VocabularyRegradePlan | undefined
|
|
145
|
+
): VocabularyRegradePlan => {
|
|
146
|
+
if (registryPlan === undefined) {
|
|
147
|
+
return latestPlan;
|
|
148
|
+
}
|
|
149
|
+
const policyClassified = [
|
|
150
|
+
...(latestPlan.scope?.policyClassified ?? []),
|
|
151
|
+
...(registryPlan.scope?.policyClassified ?? []),
|
|
152
|
+
];
|
|
153
|
+
const teachingSurfaces = [
|
|
154
|
+
...new Set([
|
|
155
|
+
...(latestPlan.scope?.teachingSurfaces ?? []),
|
|
156
|
+
...(registryPlan.scope?.teachingSurfaces ?? []),
|
|
157
|
+
]),
|
|
158
|
+
].toSorted();
|
|
159
|
+
const scope =
|
|
160
|
+
latestPlan.scope === undefined && registryPlan.scope === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: {
|
|
163
|
+
...registryPlan.scope,
|
|
164
|
+
...latestPlan.scope,
|
|
165
|
+
...(policyClassified.length === 0 ? {} : { policyClassified }),
|
|
166
|
+
...(teachingSurfaces.length === 0 ? {} : { teachingSurfaces }),
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
...registryPlan,
|
|
170
|
+
...latestPlan,
|
|
171
|
+
preserve: [
|
|
172
|
+
...(latestPlan.preserve ?? []),
|
|
173
|
+
...(registryPlan.preserve ?? []),
|
|
174
|
+
],
|
|
175
|
+
...(scope === undefined ? {} : { scope }),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const readRegradeAuditCandidates = (
|
|
180
|
+
rootDir: string,
|
|
181
|
+
selectedTransitionIds: ReadonlySet<string> | null
|
|
182
|
+
): TrailsResult<readonly RegradeAuditCandidate[], Error> => {
|
|
183
|
+
const historyDir = join(rootDir, '.trails', 'regrade', 'history');
|
|
184
|
+
if (!existsSync(historyDir)) {
|
|
185
|
+
return Result.ok([]);
|
|
186
|
+
}
|
|
187
|
+
let historyFiles: readonly string[];
|
|
188
|
+
try {
|
|
189
|
+
historyFiles = readdirSync(historyDir, { withFileTypes: true })
|
|
190
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
191
|
+
.map((entry) => join(historyDir, entry.name))
|
|
192
|
+
.toSorted();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return Result.err(
|
|
195
|
+
new InternalError('Failed to list Regrade history artifacts.', {
|
|
196
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
197
|
+
context: { historyDir },
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const registryPlansById = new Map(
|
|
202
|
+
listVocabularyRegradeAuditPlansFromRegistry().flatMap((plan) =>
|
|
203
|
+
plan.id === undefined ? [] : [[plan.id, plan] as const]
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
const selectedHistoryFileNames = selectedHistoryFileNamesFor(
|
|
207
|
+
selectedTransitionIds,
|
|
208
|
+
registryPlansById
|
|
209
|
+
);
|
|
210
|
+
const selectedHistoriesHaveCanonicalFileNames =
|
|
211
|
+
selectedTransitionIds !== null &&
|
|
212
|
+
selectedHistoryFileNames.size === selectedTransitionIds.size;
|
|
213
|
+
const candidates: RegradeAuditCandidate[] = [];
|
|
214
|
+
for (const historyFile of historyFiles) {
|
|
215
|
+
if (
|
|
216
|
+
selectedHistoriesHaveCanonicalFileNames &&
|
|
217
|
+
!selectedHistoryFileNames.has(basename(historyFile))
|
|
218
|
+
) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let rawHistory: unknown;
|
|
222
|
+
try {
|
|
223
|
+
rawHistory = JSON.parse(readFileSync(historyFile, 'utf8'));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return Result.err(
|
|
226
|
+
new InternalError('Failed to read Regrade history audit plan.', {
|
|
227
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
228
|
+
context: { historyFile },
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
!historyIsSelected(
|
|
234
|
+
rawHistory,
|
|
235
|
+
historyFile,
|
|
236
|
+
selectedTransitionIds,
|
|
237
|
+
selectedHistoryFileNames
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const history = regradeAuditHistoryPlanSchema.safeParse(rawHistory);
|
|
243
|
+
if (!history.success) {
|
|
244
|
+
return Result.err(
|
|
245
|
+
new ValidationError('Invalid Regrade history audit plan.', {
|
|
246
|
+
context: { historyFile, issues: history.error.issues },
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const latestPlan = history.data.runs.at(-1)?.plan.plan as
|
|
251
|
+
| VocabularyRegradePlan
|
|
252
|
+
| undefined;
|
|
253
|
+
if (latestPlan?.kind !== 'vocabulary') {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const transitionId = latestPlan.id ?? history.data.id;
|
|
257
|
+
if (
|
|
258
|
+
selectedTransitionIds !== null &&
|
|
259
|
+
!selectedTransitionIds.has(transitionId)
|
|
260
|
+
) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const registryPlan =
|
|
264
|
+
registryPlansById.get(transitionId) ??
|
|
265
|
+
vocabularyRegradePlanForInput(latestPlan.from, latestPlan.to) ??
|
|
266
|
+
undefined;
|
|
267
|
+
candidates.push({
|
|
268
|
+
plan: mergeAuditPlan(latestPlan, registryPlan),
|
|
269
|
+
source: rootRelativePath(rootDir, historyFile),
|
|
270
|
+
transitionId,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return Result.ok(
|
|
274
|
+
candidates.toSorted((left, right) =>
|
|
275
|
+
left.transitionId.localeCompare(right.transitionId)
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
type VocabularyPolicyClassified = NonNullable<
|
|
281
|
+
NonNullable<VocabularyRegradePlan['scope']>['policyClassified']
|
|
282
|
+
>;
|
|
283
|
+
|
|
284
|
+
export const projectPolicyClassifiedForMarkdownAudit = (
|
|
285
|
+
plan: VocabularyRegradePlan
|
|
286
|
+
): VocabularyPolicyClassified | undefined =>
|
|
287
|
+
plan.scope?.policyClassified
|
|
288
|
+
?.map((policy) => ({
|
|
289
|
+
...policy,
|
|
290
|
+
paths: policy.paths.filter(
|
|
291
|
+
(path) =>
|
|
292
|
+
!plan.scope?.exclude?.some(
|
|
293
|
+
(excludedPath) =>
|
|
294
|
+
excludedPath === path || matchesAnyPathGlob(path, [excludedPath])
|
|
295
|
+
)
|
|
296
|
+
),
|
|
297
|
+
}))
|
|
298
|
+
.filter((policy) => policy.paths.length > 0)
|
|
299
|
+
.map((policy) => {
|
|
300
|
+
const hasExplicitMarkdownPath = policy.paths.some((path) => {
|
|
301
|
+
const extension = extname(path);
|
|
302
|
+
return extension === '.md' || extension === '.mdx';
|
|
303
|
+
});
|
|
304
|
+
if (policy.expectMatches !== true || hasExplicitMarkdownPath) {
|
|
305
|
+
return policy;
|
|
306
|
+
}
|
|
307
|
+
// A Markdown-only audit cannot prove evidence for extensionless source
|
|
308
|
+
// globs. Markdown evidence requirements must name .md/.mdx explicitly.
|
|
309
|
+
const { expectMatches: _, ...markdownPolicy } = policy;
|
|
310
|
+
return markdownPolicy;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
export const projectExcludesForMarkdownAudit = (
|
|
314
|
+
plan: VocabularyRegradePlan,
|
|
315
|
+
policyClassified: VocabularyPolicyClassified | undefined
|
|
316
|
+
): readonly string[] | undefined =>
|
|
317
|
+
plan.scope?.exclude?.filter(
|
|
318
|
+
(excludedPath) =>
|
|
319
|
+
!policyClassified?.some((policy) =>
|
|
320
|
+
policy.paths.some((path) => matchesAnyPathGlob(excludedPath, [path]))
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
export const projectIncludesForMarkdownAudit = (
|
|
325
|
+
plan: VocabularyRegradePlan,
|
|
326
|
+
policyClassified: VocabularyPolicyClassified | undefined
|
|
327
|
+
): readonly string[] | undefined => {
|
|
328
|
+
if (plan.scope?.include === undefined) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
return [
|
|
332
|
+
...new Set([
|
|
333
|
+
...plan.scope.include,
|
|
334
|
+
...(policyClassified?.flatMap((policy) => policy.paths) ?? []),
|
|
335
|
+
...(plan.scope.teachingSurfaces ?? []),
|
|
336
|
+
]),
|
|
337
|
+
].toSorted();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const runRegradeAuditCandidate = async (
|
|
341
|
+
candidate: RegradeAuditCandidate,
|
|
342
|
+
input: RegradeAuditInput,
|
|
343
|
+
rootDir: string
|
|
344
|
+
): Promise<TrailsResult<RegradeAuditOutput['transitions'][number], Error>> => {
|
|
345
|
+
const policyClassified = projectPolicyClassifiedForMarkdownAudit(
|
|
346
|
+
candidate.plan
|
|
347
|
+
);
|
|
348
|
+
const exclude = projectExcludesForMarkdownAudit(
|
|
349
|
+
candidate.plan,
|
|
350
|
+
policyClassified
|
|
351
|
+
);
|
|
352
|
+
const include = projectIncludesForMarkdownAudit(
|
|
353
|
+
candidate.plan,
|
|
354
|
+
policyClassified
|
|
355
|
+
);
|
|
356
|
+
const auditPlan: VocabularyRegradePlan = {
|
|
357
|
+
...candidate.plan,
|
|
358
|
+
scope: {
|
|
359
|
+
...candidate.plan.scope,
|
|
360
|
+
extensions: ['.md', '.mdx'],
|
|
361
|
+
...(exclude === undefined ? {} : { exclude }),
|
|
362
|
+
...(include === undefined ? {} : { include }),
|
|
363
|
+
...(policyClassified === undefined ? {} : { policyClassified }),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
const preserveResult = await deriveLiveApiPreserveInventory(
|
|
367
|
+
auditPlan,
|
|
368
|
+
rootDir
|
|
369
|
+
);
|
|
370
|
+
if (preserveResult.isErr()) {
|
|
371
|
+
return preserveResult;
|
|
372
|
+
}
|
|
373
|
+
const reportResult = runVocabularyRegrade({
|
|
374
|
+
apply: false,
|
|
375
|
+
includeEntries: input.includeEntries,
|
|
376
|
+
plan: auditPlan,
|
|
377
|
+
...(preserveResult.value.length === 0
|
|
378
|
+
? {}
|
|
379
|
+
: { preserveInventory: preserveResult.value }),
|
|
380
|
+
root: rootDir,
|
|
381
|
+
});
|
|
382
|
+
if (reportResult.isErr()) {
|
|
383
|
+
return reportResult;
|
|
384
|
+
}
|
|
385
|
+
if (reportResult.value === null) {
|
|
386
|
+
return Result.err(
|
|
387
|
+
new NotFoundError('Regrade audit root could not be collected.', {
|
|
388
|
+
context: { rootDir },
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const { run } = reportResult.value;
|
|
393
|
+
if (run === undefined) {
|
|
394
|
+
return Result.err(
|
|
395
|
+
new InternalError('Regrade audit did not produce a vocabulary run.', {
|
|
396
|
+
context: { transitionId: candidate.transitionId },
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const output = regradeAuditTransitionSchema.safeParse({
|
|
401
|
+
report: {
|
|
402
|
+
dispositions: run.report.dispositions,
|
|
403
|
+
entries: reportResult.value.entries,
|
|
404
|
+
occurrences: run.ledger.occurrences.length,
|
|
405
|
+
open: run.report.open,
|
|
406
|
+
scanned: reportResult.value.scanned,
|
|
407
|
+
status: run.report.gate.status,
|
|
408
|
+
},
|
|
409
|
+
source: candidate.source,
|
|
410
|
+
transitionId: candidate.transitionId,
|
|
411
|
+
});
|
|
412
|
+
if (!output.success) {
|
|
413
|
+
return Result.err(
|
|
414
|
+
new InternalError('Regrade audit produced an invalid transition.', {
|
|
415
|
+
context: {
|
|
416
|
+
issues: output.error.issues,
|
|
417
|
+
transitionId: candidate.transitionId,
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return Result.ok(output.data);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export const auditRegradeHistory = async (
|
|
426
|
+
input: RegradeAuditInput,
|
|
427
|
+
rootDir: string
|
|
428
|
+
): Promise<TrailsResult<RegradeAuditOutput, Error>> => {
|
|
429
|
+
const selectedTransitionIds =
|
|
430
|
+
input.transitionIds === undefined ? null : new Set(input.transitionIds);
|
|
431
|
+
const candidatesResult = readRegradeAuditCandidates(
|
|
432
|
+
rootDir,
|
|
433
|
+
selectedTransitionIds
|
|
434
|
+
);
|
|
435
|
+
if (candidatesResult.isErr()) {
|
|
436
|
+
return candidatesResult;
|
|
437
|
+
}
|
|
438
|
+
const candidates = candidatesResult.value;
|
|
439
|
+
if (selectedTransitionIds !== null) {
|
|
440
|
+
const auditedTransitionIds = new Set(
|
|
441
|
+
candidates.map((candidate) => candidate.transitionId)
|
|
442
|
+
);
|
|
443
|
+
const missingTransitionIds = [...selectedTransitionIds].filter(
|
|
444
|
+
(transitionId) => !auditedTransitionIds.has(transitionId)
|
|
445
|
+
);
|
|
446
|
+
if (missingTransitionIds.length > 0) {
|
|
447
|
+
return Result.err(
|
|
448
|
+
new ValidationError(
|
|
449
|
+
'Selected Regrade audit transitions do not have committed history.',
|
|
450
|
+
{ context: { transitionIds: missingTransitionIds.toSorted() } }
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const transitions: RegradeAuditOutput['transitions'][number][] = [];
|
|
456
|
+
for (const candidate of candidates) {
|
|
457
|
+
const transitionResult = await runRegradeAuditCandidate(
|
|
458
|
+
candidate,
|
|
459
|
+
input,
|
|
460
|
+
rootDir
|
|
461
|
+
);
|
|
462
|
+
if (transitionResult.isErr()) {
|
|
463
|
+
return transitionResult;
|
|
464
|
+
}
|
|
465
|
+
transitions.push(transitionResult.value);
|
|
466
|
+
}
|
|
467
|
+
const open = transitions.reduce(
|
|
468
|
+
(total, transition) => total + transition.report.open,
|
|
469
|
+
0
|
|
470
|
+
);
|
|
471
|
+
return Result.ok({
|
|
472
|
+
gate: {
|
|
473
|
+
open,
|
|
474
|
+
status: transitions.some(
|
|
475
|
+
(transition) => transition.report.status === 'open'
|
|
476
|
+
)
|
|
477
|
+
? 'open'
|
|
478
|
+
: 'green',
|
|
479
|
+
},
|
|
480
|
+
transitions,
|
|
481
|
+
});
|
|
482
|
+
};
|