@ontrails/trails 0.2.0
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 +1906 -0
- package/README.md +48 -0
- package/bin/trails.ts +3 -0
- package/package.json +57 -0
- package/src/app.ts +167 -0
- package/src/clack.ts +111 -0
- package/src/cli.ts +308 -0
- package/src/completions.ts +431 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +129 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +515 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/changeset-packages.ts +99 -0
- package/src/release/check.ts +1191 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +221 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +255 -0
- package/src/release/lock-roundtrip-workspace.ts +107 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +848 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1780 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/stable-version-release.ts +80 -0
- package/src/release/wayfinder-dogfood-smoke.ts +762 -0
- package/src/release/zero-line-transition.ts +68 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-argv.ts +133 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch-project.ts +52 -0
- package/src/run-watch.ts +381 -0
- package/src/run-wayfind-outline.ts +170 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +816 -0
- package/src/trails/add-trail.ts +141 -0
- package/src/trails/add-verify.ts +252 -0
- package/src/trails/compile.ts +118 -0
- package/src/trails/completions-complete.ts +236 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/config-explain.ts +43 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +1215 -0
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +652 -0
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +80 -0
- package/src/trails/dev-reset.ts +48 -0
- package/src/trails/dev-stats.ts +71 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +106 -0
- package/src/trails/load-app.ts +1145 -0
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project-context-output.ts +304 -0
- package/src/trails/project-context.ts +613 -0
- package/src/trails/project.ts +65 -0
- package/src/trails/regrade.ts +4951 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +592 -0
- package/src/trails/run-examples.ts +149 -0
- package/src/trails/run.ts +496 -0
- package/src/trails/scaffold-json.ts +60 -0
- package/src/trails/scaffold-topo-identity.ts +479 -0
- package/src/trails/survey.ts +990 -0
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +247 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +348 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +598 -0
- package/src/trails/wayfind-diff.ts +716 -0
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1319 -0
- package/src/versions.ts +31 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `warden` trail -- Governance checks.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the shared @ontrails/warden command surface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { posix } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { Result, trail, ValidationError } from '@ontrails/core';
|
|
10
|
+
import {
|
|
11
|
+
diagnosticSchema,
|
|
12
|
+
runWardenCommand,
|
|
13
|
+
wardenDepthValues,
|
|
14
|
+
wardenDraftsValues,
|
|
15
|
+
wardenFailOnValues,
|
|
16
|
+
wardenFormatValues,
|
|
17
|
+
wardenLockValues,
|
|
18
|
+
} from '@ontrails/warden';
|
|
19
|
+
import type {
|
|
20
|
+
EffectiveWardenConfig,
|
|
21
|
+
WardenExpectedAppBinding,
|
|
22
|
+
WardenTopoDriftResult,
|
|
23
|
+
} from '@ontrails/warden';
|
|
24
|
+
import { deriveWorkspaceView } from '@ontrails/topography';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
assertObservableProjectApps,
|
|
29
|
+
resolveOperatorProjectContext,
|
|
30
|
+
} from './project-context.js';
|
|
31
|
+
import type {
|
|
32
|
+
OperatorProjectApp,
|
|
33
|
+
OperatorProjectContext,
|
|
34
|
+
} from './project-context.js';
|
|
35
|
+
import {
|
|
36
|
+
operatorProjectContextOutput,
|
|
37
|
+
operatorProjectContextOutputSchema,
|
|
38
|
+
} from './project-context-output.js';
|
|
39
|
+
import type { OperatorProjectContextOutput } from './project-context-output.js';
|
|
40
|
+
import { markCurrentAppExampleInput } from './topo-support.js';
|
|
41
|
+
|
|
42
|
+
const wardenWorkspaceAppEvidenceSchema = z.object({
|
|
43
|
+
appId: z.string(),
|
|
44
|
+
appRoot: z.string(),
|
|
45
|
+
artifactPath: z.string(),
|
|
46
|
+
binding: z.enum(['matched', 'unobserved']),
|
|
47
|
+
blockedReason: z.string().optional(),
|
|
48
|
+
committedHash: z.string().nullable().optional(),
|
|
49
|
+
currentHash: z.string().optional(),
|
|
50
|
+
freshness: z.enum(['fresh', 'stale', 'unavailable', 'unobserved']),
|
|
51
|
+
modulePath: z.string(),
|
|
52
|
+
moduleSource: z.enum(['config', 'convention', 'module']),
|
|
53
|
+
provenance: z.literal('warden-app-drift'),
|
|
54
|
+
status: z.enum(['available', 'missing', 'unobserved']),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const wardenWorkspaceEvidenceSchema = z.object({
|
|
58
|
+
apps: z.array(wardenWorkspaceAppEvidenceSchema).readonly(),
|
|
59
|
+
completeness: z.enum(['complete', 'partial']),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
type WardenWorkspaceEvidence = z.output<typeof wardenWorkspaceEvidenceSchema>;
|
|
63
|
+
|
|
64
|
+
const assertWardenArtifactBindings = async (
|
|
65
|
+
context: OperatorProjectContext
|
|
66
|
+
): Promise<Result<void, ValidationError>> => {
|
|
67
|
+
if (context.identity.workspace === undefined) {
|
|
68
|
+
return Result.ok();
|
|
69
|
+
}
|
|
70
|
+
const selectedAppIds =
|
|
71
|
+
context.selectedExtent === 'workspace'
|
|
72
|
+
? context.apps.map((app) => app.id as string)
|
|
73
|
+
: [context.app.id as string];
|
|
74
|
+
try {
|
|
75
|
+
const view = await deriveWorkspaceView({
|
|
76
|
+
identity: context.identity,
|
|
77
|
+
selectedAppIds,
|
|
78
|
+
});
|
|
79
|
+
const invalid = view.evidence.apps.filter(
|
|
80
|
+
(app) =>
|
|
81
|
+
selectedAppIds.includes(app.id) &&
|
|
82
|
+
(app.binding === 'mismatched' || app.status === 'invalid')
|
|
83
|
+
);
|
|
84
|
+
return invalid.length === 0
|
|
85
|
+
? Result.ok()
|
|
86
|
+
: Result.err(
|
|
87
|
+
new ValidationError(
|
|
88
|
+
`Saved artifact evidence is invalid for Config-owned app selection: ${invalid.map((app) => app.id).join(', ')}.`,
|
|
89
|
+
{
|
|
90
|
+
context: {
|
|
91
|
+
appIds: selectedAppIds,
|
|
92
|
+
evidence: invalid,
|
|
93
|
+
projectRoot: context.projectRoot,
|
|
94
|
+
reason: 'invalid-binding',
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return Result.err(
|
|
101
|
+
new ValidationError(
|
|
102
|
+
'Unable to inspect saved artifact identity before Warden execution.',
|
|
103
|
+
{
|
|
104
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
105
|
+
context: {
|
|
106
|
+
appIds: selectedAppIds,
|
|
107
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
108
|
+
projectRoot: context.projectRoot,
|
|
109
|
+
reason: 'invalid-binding',
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const assertWardenArtifactPreflight = async (
|
|
118
|
+
context: OperatorProjectContext
|
|
119
|
+
): Promise<Result<void, ValidationError>> => {
|
|
120
|
+
const observable = await assertObservableProjectApps(context);
|
|
121
|
+
return observable.isErr()
|
|
122
|
+
? observable
|
|
123
|
+
: await assertWardenArtifactBindings(context);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Report whether a resolved Warden run can reach saved lock evidence.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately over-approximates the shared CLI drift gate (`shouldRunDrift`
|
|
130
|
+
* in `packages/warden/src/cli.ts`, which on this path only fires at depth
|
|
131
|
+
* `all` with the lock not skipped): lock skipping opts out entirely, and both
|
|
132
|
+
* topo-aware depths otherwise preflight so no lock-reading depth can drift out
|
|
133
|
+
* of coverage. Fixing is not an exemption — a `--fix` run at the default depth
|
|
134
|
+
* still reports drift evidence, so it needs the same artifact preflight as a
|
|
135
|
+
* reporting run.
|
|
136
|
+
*/
|
|
137
|
+
const consumesSavedArtifactEvidence = (
|
|
138
|
+
effectiveConfig: EffectiveWardenConfig | undefined
|
|
139
|
+
): boolean => {
|
|
140
|
+
if (effectiveConfig === undefined) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (effectiveConfig.lock === 'skip') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return effectiveConfig.depth === 'topo' || effectiveConfig.depth === 'all';
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Trail definition
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const wardenInputSchema = z
|
|
154
|
+
.object({
|
|
155
|
+
adapterCheck: z
|
|
156
|
+
.boolean()
|
|
157
|
+
.default(false)
|
|
158
|
+
.describe('Run shared adapter authoring checks'),
|
|
159
|
+
app: z.string().optional().describe('Configured workspace app ID'),
|
|
160
|
+
apps: z
|
|
161
|
+
.array(z.string())
|
|
162
|
+
.optional()
|
|
163
|
+
.describe('Legacy app names or module paths to govern'),
|
|
164
|
+
cached: z.boolean().default(false).describe('Alias for --lock cached'),
|
|
165
|
+
ci: z.boolean().default(false).describe('Use the CI Warden preset'),
|
|
166
|
+
configPath: z.string().optional().describe('Path to trails.config.ts'),
|
|
167
|
+
depth: z
|
|
168
|
+
.enum(wardenDepthValues)
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Cumulative analysis depth'),
|
|
171
|
+
drafts: z.enum(wardenDraftsValues).optional().describe('Draft state mode'),
|
|
172
|
+
excludeDrafts: z
|
|
173
|
+
.boolean()
|
|
174
|
+
.default(false)
|
|
175
|
+
.describe('Alias for --drafts exclude'),
|
|
176
|
+
failOn: z.enum(wardenFailOnValues).optional().describe('Failure threshold'),
|
|
177
|
+
fix: z.boolean().default(false).describe('Apply safe source fixes'),
|
|
178
|
+
format: z.enum(wardenFormatValues).optional().describe('Output format'),
|
|
179
|
+
github: z.boolean().default(false).describe('Alias for --format github'),
|
|
180
|
+
includeDrafts: z
|
|
181
|
+
.boolean()
|
|
182
|
+
.default(false)
|
|
183
|
+
.describe('Alias for --drafts include'),
|
|
184
|
+
json: z.boolean().default(false).describe('Alias for --format json'),
|
|
185
|
+
lock: z.enum(wardenLockValues).optional().describe('Lockfile mode'),
|
|
186
|
+
noLockMutation: z
|
|
187
|
+
.boolean()
|
|
188
|
+
.default(false)
|
|
189
|
+
.describe('Suppress lockfile mutation'),
|
|
190
|
+
onlyDrafts: z.boolean().default(false).describe('Alias for --drafts only'),
|
|
191
|
+
prePush: z.boolean().default(false).describe('Use the pre-push preset'),
|
|
192
|
+
refresh: z.boolean().default(false).describe('Alias for --lock refresh'),
|
|
193
|
+
rootDir: z.string().optional().describe('Root directory to scan'),
|
|
194
|
+
scopeExclude: z
|
|
195
|
+
.array(z.string())
|
|
196
|
+
.optional()
|
|
197
|
+
.describe('Root-relative path globs that Warden should not govern'),
|
|
198
|
+
skipLock: z.boolean().default(false).describe('Alias for --lock skip'),
|
|
199
|
+
strict: z.boolean().default(false).describe('Alias for --fail-on warning'),
|
|
200
|
+
summary: z.boolean().default(false).describe('Alias for --format summary'),
|
|
201
|
+
})
|
|
202
|
+
.refine((input) => input.app === undefined || input.apps === undefined, {
|
|
203
|
+
message:
|
|
204
|
+
'Use --app for Config-owned app selection; do not combine it with legacy --apps.',
|
|
205
|
+
path: ['app'],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
type WardenTrailInput = z.infer<typeof wardenInputSchema>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Report whether an authored input pins lock skipping through any alias.
|
|
212
|
+
*
|
|
213
|
+
* Mirrors the lock alias precedence in {@link buildWardenCommandArgs}, where
|
|
214
|
+
* `--skip-lock` outranks `--refresh` and `--cached`, which in turn outrank an
|
|
215
|
+
* explicit `--lock` value.
|
|
216
|
+
*/
|
|
217
|
+
const inputSkipsLockEvidence = (input: WardenTrailInput): boolean => {
|
|
218
|
+
if (input.skipLock === true) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (input.refresh === true || input.cached === true) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return input.lock === 'skip';
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Report whether a fixing run must preflight saved artifacts before Warden
|
|
229
|
+
* writes any patched source file.
|
|
230
|
+
*
|
|
231
|
+
* The shared runner applies safe fixes inside `runWarden` itself
|
|
232
|
+
* (`applySafeFixesToFiles` in `packages/warden/src/cli.ts`), so the post-run
|
|
233
|
+
* {@link consumesSavedArtifactEvidence} gate cannot protect source: by the time
|
|
234
|
+
* `report.effectiveConfig` exists the files have already been rewritten, and
|
|
235
|
+
* those fixes were selected against the very topo evidence the gate validates.
|
|
236
|
+
* This input-level predicate therefore runs before the command and deliberately
|
|
237
|
+
* over-approximates the post-run gate: it exempts only an input that explicitly
|
|
238
|
+
* pins both lock skipping and `source` depth. Presets cannot widen that
|
|
239
|
+
* exemption -- `parseWardenCommandArgs` applies `--pre-push`/`--ci` in a first
|
|
240
|
+
* token pass and explicit options in a second, so an explicit `--depth source`
|
|
241
|
+
* always wins -- and the CLI layer outranks the config and environment layers
|
|
242
|
+
* in `resolveWardenConfig`. Every other fixing input preflights, including
|
|
243
|
+
* inputs whose resolved depth would never have read a lock.
|
|
244
|
+
*
|
|
245
|
+
* Moving this gate to its single owner -- a preflight hook inside `runWarden`,
|
|
246
|
+
* so the shared runner validates evidence before its own writes -- is tracked
|
|
247
|
+
* as TRL-1322.
|
|
248
|
+
*/
|
|
249
|
+
const fixRunNeedsArtifactPreflight = (input: WardenTrailInput): boolean =>
|
|
250
|
+
input.fix === true &&
|
|
251
|
+
!(inputSkipsLockEvidence(input) && input.depth === 'source');
|
|
252
|
+
|
|
253
|
+
const pushFlag = (args: string[], condition: boolean, flag: string): void => {
|
|
254
|
+
if (condition) {
|
|
255
|
+
args.push(flag);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const pushValue = (
|
|
260
|
+
args: string[],
|
|
261
|
+
flag: string,
|
|
262
|
+
value: string | undefined
|
|
263
|
+
): void => {
|
|
264
|
+
if (value !== undefined) {
|
|
265
|
+
args.push(flag, value);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const pushApps = (
|
|
270
|
+
args: string[],
|
|
271
|
+
apps: readonly string[] | undefined
|
|
272
|
+
): void => {
|
|
273
|
+
if (apps !== undefined && apps.length > 0) {
|
|
274
|
+
args.push('--apps', apps.join(','));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const pushRepeatedValues = (
|
|
279
|
+
args: string[],
|
|
280
|
+
flag: string,
|
|
281
|
+
values: readonly string[] | undefined
|
|
282
|
+
): void => {
|
|
283
|
+
for (const value of values ?? []) {
|
|
284
|
+
args.push(flag, value);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const buildWardenCommandArgs = (
|
|
289
|
+
input: WardenTrailInput
|
|
290
|
+
): readonly string[] => {
|
|
291
|
+
const args: string[] = [];
|
|
292
|
+
|
|
293
|
+
pushFlag(args, input.prePush, '--pre-push');
|
|
294
|
+
pushFlag(args, input.ci, '--ci');
|
|
295
|
+
pushValue(args, '--depth', input.depth);
|
|
296
|
+
if (input.strict) {
|
|
297
|
+
args.push('--strict');
|
|
298
|
+
} else {
|
|
299
|
+
pushValue(args, '--fail-on', input.failOn);
|
|
300
|
+
}
|
|
301
|
+
if (input.github) {
|
|
302
|
+
args.push('--github');
|
|
303
|
+
} else if (input.json) {
|
|
304
|
+
args.push('--json');
|
|
305
|
+
} else if (input.summary) {
|
|
306
|
+
args.push('--summary');
|
|
307
|
+
} else {
|
|
308
|
+
pushValue(args, '--format', input.format);
|
|
309
|
+
}
|
|
310
|
+
if (input.skipLock) {
|
|
311
|
+
args.push('--skip-lock');
|
|
312
|
+
} else if (input.refresh) {
|
|
313
|
+
args.push('--refresh');
|
|
314
|
+
} else if (input.cached) {
|
|
315
|
+
args.push('--cached');
|
|
316
|
+
} else {
|
|
317
|
+
pushValue(args, '--lock', input.lock);
|
|
318
|
+
}
|
|
319
|
+
if (input.onlyDrafts) {
|
|
320
|
+
args.push('--only-drafts');
|
|
321
|
+
} else if (input.excludeDrafts) {
|
|
322
|
+
args.push('--exclude-drafts');
|
|
323
|
+
} else if (input.includeDrafts) {
|
|
324
|
+
args.push('--include-drafts');
|
|
325
|
+
} else {
|
|
326
|
+
pushValue(args, '--drafts', input.drafts);
|
|
327
|
+
}
|
|
328
|
+
pushFlag(args, input.noLockMutation, '--no-lock-mutation');
|
|
329
|
+
pushFlag(args, input.fix, '--fix');
|
|
330
|
+
pushFlag(args, input.adapterCheck, '--adapter-check');
|
|
331
|
+
pushValue(args, '--config-path', input.configPath);
|
|
332
|
+
pushRepeatedValues(args, '--scope-exclude', input.scopeExclude);
|
|
333
|
+
pushApps(args, input.apps);
|
|
334
|
+
|
|
335
|
+
return args;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const projectRelativeModulePath = (app: OperatorProjectApp): string =>
|
|
339
|
+
app.configured ? posix.join(app.root, app.modulePath) : app.modulePath;
|
|
340
|
+
|
|
341
|
+
interface SelectedWardenApps {
|
|
342
|
+
readonly apps: readonly string[] | undefined;
|
|
343
|
+
readonly expectedAppBindings: readonly WardenExpectedAppBinding[] | undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const selectedWardenApps = (
|
|
347
|
+
context: OperatorProjectContext,
|
|
348
|
+
legacyApps: readonly string[] | undefined
|
|
349
|
+
): Result<SelectedWardenApps, ValidationError> => {
|
|
350
|
+
if (context.identity.workspace === undefined) {
|
|
351
|
+
return Result.ok({
|
|
352
|
+
apps: legacyApps,
|
|
353
|
+
expectedAppBindings: undefined,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (legacyApps !== undefined) {
|
|
357
|
+
return Result.err(
|
|
358
|
+
new ValidationError(
|
|
359
|
+
'Configured workspaces derive Warden app targets from workspace.apps. Use --app <id> to narrow one invocation; legacy --apps is not a second app catalog.',
|
|
360
|
+
{
|
|
361
|
+
context: {
|
|
362
|
+
configuredAppIds: context.identity.apps.map((app) => app.id),
|
|
363
|
+
reason: 'invalid-binding',
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const selectedApps =
|
|
370
|
+
context.selectedExtent === 'workspace' ? context.apps : [context.app];
|
|
371
|
+
const bindings: WardenExpectedAppBinding[] = [];
|
|
372
|
+
for (const app of selectedApps) {
|
|
373
|
+
if (app.id === undefined) {
|
|
374
|
+
return Result.err(
|
|
375
|
+
new ValidationError(
|
|
376
|
+
'Configured Warden app selection is missing its Config-owned identity.',
|
|
377
|
+
{ context: { appRoot: app.root, reason: 'invalid-binding' } }
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
bindings.push({
|
|
382
|
+
app: projectRelativeModulePath(app),
|
|
383
|
+
expectedAppId: app.id,
|
|
384
|
+
rootDir: app.rootDir,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return Result.ok({
|
|
388
|
+
apps: bindings.map((binding) => binding.app),
|
|
389
|
+
expectedAppBindings: bindings,
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const renderWardenSelection = (
|
|
394
|
+
formatted: string,
|
|
395
|
+
format: 'github' | 'json' | 'summary',
|
|
396
|
+
project: OperatorProjectContextOutput,
|
|
397
|
+
workspaceEvidence?: WardenWorkspaceEvidence | undefined
|
|
398
|
+
): Result<string, ValidationError> => {
|
|
399
|
+
if (format === 'json') {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(formatted) as Record<string, unknown>;
|
|
402
|
+
return Result.ok(
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
...parsed,
|
|
405
|
+
project,
|
|
406
|
+
...(workspaceEvidence === undefined ? {} : { workspaceEvidence }),
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return Result.err(
|
|
411
|
+
new ValidationError(
|
|
412
|
+
'Warden returned invalid JSON output.',
|
|
413
|
+
error instanceof Error ? { cause: error } : {}
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const selection = `Selection: ${project.selectedExtent} (${project.selectionProvenance}) at ${project.projectRoot}`;
|
|
419
|
+
return Result.ok(
|
|
420
|
+
formatted.length === 0 ? selection : `${selection}\n${formatted}`
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const deriveWardenWorkspaceEvidence = (
|
|
425
|
+
context: OperatorProjectContext,
|
|
426
|
+
topoDrift: readonly WardenTopoDriftResult[] | undefined
|
|
427
|
+
): WardenWorkspaceEvidence | undefined => {
|
|
428
|
+
if (context.selectedExtent !== 'workspace') {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
const observations = new Map(
|
|
432
|
+
(topoDrift ?? []).map((entry) => [entry.name, entry.drift])
|
|
433
|
+
);
|
|
434
|
+
const apps = context.apps.map((app) => {
|
|
435
|
+
const drift = observations.get(app.id as string);
|
|
436
|
+
let status: WardenWorkspaceEvidence['apps'][number]['status'];
|
|
437
|
+
let freshness: WardenWorkspaceEvidence['apps'][number]['freshness'];
|
|
438
|
+
if (drift === undefined) {
|
|
439
|
+
status = 'unobserved';
|
|
440
|
+
freshness = 'unobserved';
|
|
441
|
+
} else if (drift.committedHash === null) {
|
|
442
|
+
status = 'missing';
|
|
443
|
+
freshness = 'unavailable';
|
|
444
|
+
} else {
|
|
445
|
+
status = 'available';
|
|
446
|
+
freshness = drift.stale ? 'stale' : 'fresh';
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
appId: app.id as string,
|
|
450
|
+
appRoot: app.root,
|
|
451
|
+
artifactPath: app.lockPath,
|
|
452
|
+
binding:
|
|
453
|
+
drift === undefined ? ('unobserved' as const) : ('matched' as const),
|
|
454
|
+
...(drift?.blockedReason === undefined
|
|
455
|
+
? {}
|
|
456
|
+
: { blockedReason: drift.blockedReason }),
|
|
457
|
+
...(drift === undefined
|
|
458
|
+
? {}
|
|
459
|
+
: {
|
|
460
|
+
committedHash: drift.committedHash,
|
|
461
|
+
currentHash: drift.currentHash,
|
|
462
|
+
}),
|
|
463
|
+
freshness,
|
|
464
|
+
modulePath: app.modulePath,
|
|
465
|
+
moduleSource: app.moduleSource,
|
|
466
|
+
provenance: 'warden-app-drift' as const,
|
|
467
|
+
status,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
apps: apps.toSorted((left, right) => left.appId.localeCompare(right.appId)),
|
|
472
|
+
completeness: apps.every((app) => app.status === 'available')
|
|
473
|
+
? 'complete'
|
|
474
|
+
: 'partial',
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const wardenTrail = trail('warden', {
|
|
479
|
+
description: 'Run governance checks (lint + drift)',
|
|
480
|
+
examples: [
|
|
481
|
+
{
|
|
482
|
+
input: markCurrentAppExampleInput(
|
|
483
|
+
{
|
|
484
|
+
depth: 'source',
|
|
485
|
+
lock: 'skip',
|
|
486
|
+
rootDir: '.',
|
|
487
|
+
},
|
|
488
|
+
{ selection: 'configured-project' }
|
|
489
|
+
),
|
|
490
|
+
name: 'Default warden run',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
input: markCurrentAppExampleInput(
|
|
494
|
+
{
|
|
495
|
+
depth: 'source',
|
|
496
|
+
format: 'github',
|
|
497
|
+
lock: 'skip',
|
|
498
|
+
rootDir: '.',
|
|
499
|
+
},
|
|
500
|
+
{ selection: 'configured-project' }
|
|
501
|
+
),
|
|
502
|
+
name: 'GitHub Actions annotations',
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
implementation: async (input, ctx) => {
|
|
506
|
+
const contextResult = await resolveOperatorProjectContext(input, {
|
|
507
|
+
cwd: ctx.cwd,
|
|
508
|
+
});
|
|
509
|
+
if (contextResult.isErr()) {
|
|
510
|
+
return contextResult;
|
|
511
|
+
}
|
|
512
|
+
const context = contextResult.value;
|
|
513
|
+
const apps = selectedWardenApps(context, input.apps);
|
|
514
|
+
if (apps.isErr()) {
|
|
515
|
+
return apps;
|
|
516
|
+
}
|
|
517
|
+
if (fixRunNeedsArtifactPreflight(input)) {
|
|
518
|
+
const fixPreflight = await assertWardenArtifactPreflight(context);
|
|
519
|
+
if (fixPreflight.isErr()) {
|
|
520
|
+
return fixPreflight;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const result = await runWardenCommand({
|
|
524
|
+
args: buildWardenCommandArgs({
|
|
525
|
+
...input,
|
|
526
|
+
apps: apps.value.apps === undefined ? undefined : [...apps.value.apps],
|
|
527
|
+
}),
|
|
528
|
+
cwd: context.projectRoot,
|
|
529
|
+
env: ctx.env ?? {},
|
|
530
|
+
expectedAppBindings: apps.value.expectedAppBindings,
|
|
531
|
+
});
|
|
532
|
+
if (result.preflightError !== undefined) {
|
|
533
|
+
return Result.err(result.preflightError);
|
|
534
|
+
}
|
|
535
|
+
const { report } = result;
|
|
536
|
+
if (consumesSavedArtifactEvidence(report.effectiveConfig)) {
|
|
537
|
+
const preflight = await assertWardenArtifactPreflight(context);
|
|
538
|
+
if (preflight.isErr()) {
|
|
539
|
+
return preflight;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const project = operatorProjectContextOutput(context);
|
|
543
|
+
const workspaceEvidence = deriveWardenWorkspaceEvidence(
|
|
544
|
+
context,
|
|
545
|
+
report.topoDrift
|
|
546
|
+
);
|
|
547
|
+
const formatted = renderWardenSelection(
|
|
548
|
+
result.output,
|
|
549
|
+
report.effectiveConfig?.format ?? 'summary',
|
|
550
|
+
project,
|
|
551
|
+
workspaceEvidence
|
|
552
|
+
);
|
|
553
|
+
if (formatted.isErr()) {
|
|
554
|
+
return formatted;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return Result.ok({
|
|
558
|
+
diagnostics: [...report.diagnostics],
|
|
559
|
+
drift: report.drift,
|
|
560
|
+
errorCount: report.errorCount,
|
|
561
|
+
fixes: report.fixes,
|
|
562
|
+
formatted: formatted.value,
|
|
563
|
+
passed: report.passed,
|
|
564
|
+
project,
|
|
565
|
+
...(workspaceEvidence === undefined ? {} : { workspaceEvidence }),
|
|
566
|
+
warnCount: report.warnCount,
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
input: wardenInputSchema,
|
|
570
|
+
intent: 'write',
|
|
571
|
+
output: z.object({
|
|
572
|
+
diagnostics: z.array(
|
|
573
|
+
diagnosticSchema.extend({ topoName: z.string().optional() })
|
|
574
|
+
),
|
|
575
|
+
drift: z
|
|
576
|
+
.object({
|
|
577
|
+
blockedReason: z.string().optional(),
|
|
578
|
+
committedHash: z.string().nullable(),
|
|
579
|
+
currentHash: z.string(),
|
|
580
|
+
stale: z.boolean(),
|
|
581
|
+
})
|
|
582
|
+
.nullable(),
|
|
583
|
+
errorCount: z.number(),
|
|
584
|
+
fixes: z
|
|
585
|
+
.object({
|
|
586
|
+
applied: z.number(),
|
|
587
|
+
filesChanged: z.number(),
|
|
588
|
+
skipped: z.number(),
|
|
589
|
+
})
|
|
590
|
+
.optional(),
|
|
591
|
+
formatted: z.string(),
|
|
592
|
+
passed: z.boolean(),
|
|
593
|
+
project: operatorProjectContextOutputSchema,
|
|
594
|
+
warnCount: z.number(),
|
|
595
|
+
workspaceEvidence: wardenWorkspaceEvidenceSchema.optional(),
|
|
596
|
+
}),
|
|
597
|
+
permit: 'public',
|
|
598
|
+
});
|