paseo-req-kit 1.8.0 → 1.9.0-beta.1
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 +17 -0
- package/bin/req.mjs +19 -0
- package/contracts/README.md +2 -0
- package/contracts/cli-json.md +66 -0
- package/contracts/usage-record.md +48 -0
- package/docs/adr/0026-machine-records-and-api.md +23 -0
- package/docs/adr/README.md +1 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +2 -0
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +10 -0
- package/engine/src/api.ts +397 -0
- package/engine/src/control.ts +2 -2
- package/engine/src/eval.ts +2 -0
- package/engine/src/index-cmd.ts +33 -0
- package/engine/src/main.ts +26 -0
- package/engine/src/records.ts +271 -0
- package/engine/src/state.ts +3 -0
- package/engine/src/stats.ts +479 -0
- package/package.json +2 -2
package/engine/src/eval.ts
CHANGED
|
@@ -104,6 +104,8 @@ function runPipeline(repo: string, featureDir: string, c: EvalCase, configPath:
|
|
|
104
104
|
const env = { ...process.env };
|
|
105
105
|
delete env.PASEO_AGENT_ID;
|
|
106
106
|
delete env.PASEO_AGENT_CWD;
|
|
107
|
+
// Evaluation runs are not real requirements: keep them out of the machine's index and usage records.
|
|
108
|
+
env.REQ_KIT_NO_RECORD = "1";
|
|
107
109
|
const run = spawnSync(process.execPath, args, { cwd: repo, env, encoding: "utf8", timeout: timeoutMs, windowsHide: true });
|
|
108
110
|
fs.appendFileSync(path.join(repo, featureDir, ".pipeline", "eval-run.log"), `${run.stdout ?? ""}${run.stderr ?? ""}`);
|
|
109
111
|
if (run.error) return `流水线进程出错:${run.error.message}`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `req index`: the machine's list of requirements (records.ts), for people.
|
|
2
|
+
// req index what is recorded
|
|
3
|
+
// req index --rebuild [<dir>...] record the requirements that have run in these repositories
|
|
4
|
+
// (default: here) and in those already recorded
|
|
5
|
+
// req index --prune forget the requirements whose directory is gone
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { apiIndex } from "./api.js";
|
|
9
|
+
import { indexDir, readIndex } from "./records.js";
|
|
10
|
+
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
const prune = argv.includes("--prune");
|
|
13
|
+
const rebuildAt = argv.indexOf("--rebuild");
|
|
14
|
+
const dirs = rebuildAt < 0 ? [] : argv.slice(rebuildAt + 1).filter((a) => !a.startsWith("--"));
|
|
15
|
+
if (rebuildAt >= 0 && dirs.length === 0) dirs.push(process.cwd());
|
|
16
|
+
|
|
17
|
+
if (rebuildAt >= 0 || prune) {
|
|
18
|
+
const r = apiIndex({ rebuild: dirs, prune });
|
|
19
|
+
if (rebuildAt >= 0) console.log(`扫描了 ${r.scanned.length} 个仓库,登记 ${r.registered} 个运行过的需求。`);
|
|
20
|
+
if (prune) console.log(`移除了 ${r.pruned} 个目录已不在的需求。`);
|
|
21
|
+
console.log(`本机共记录 ${r.total} 个需求(${indexDir()})。`);
|
|
22
|
+
} else {
|
|
23
|
+
const entries = readIndex();
|
|
24
|
+
if (entries.length === 0) {
|
|
25
|
+
console.log("本机还没有记录任何需求。1.9 之前运行过的需求:在项目目录运行 req index --rebuild。");
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`本机记录的需求(${entries.length} 个;查看进度用 req api list --all 或 Paseo 插件):`);
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
console.log(`- ${e.project}:${path.basename(e.featureDir)}${e.present ? "" : "(目录已不在,req index --prune 可以移除)"}`);
|
|
30
|
+
console.log(` ${path.join(e.repo, e.featureDir)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/engine/src/main.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// from the chat agent's shell; can also be run in the foreground for debugging.
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { approveCommand, LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
|
|
7
8
|
import { dequeuedMessage, queuedMessage, watchdogPrompt } from "./messages.js";
|
|
@@ -13,6 +14,7 @@ import { satisfies } from "./daemon-check.js";
|
|
|
13
14
|
import { describeAgentProfile, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
14
15
|
import { jointProblem } from "./joint.js";
|
|
15
16
|
import { runPipeline } from "./pipeline.js";
|
|
17
|
+
import { appendRun, runRecord } from "./records.js";
|
|
16
18
|
import { preflight, verifyPhaseProblem } from "./preflight.js";
|
|
17
19
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
18
20
|
import {
|
|
@@ -243,6 +245,28 @@ async function main(): Promise<number> {
|
|
|
243
245
|
writeState(stateFile, state);
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
// This run's usage record (1.9): written once when the process ends, however it ends.
|
|
249
|
+
let recorded = false;
|
|
250
|
+
const recordRun = () => {
|
|
251
|
+
if (recorded) return;
|
|
252
|
+
recorded = true;
|
|
253
|
+
const title = (() => {
|
|
254
|
+
try {
|
|
255
|
+
return (/^#\s+(.+)$/m.exec(fs.readFileSync(checked.specPath, "utf8"))?.[1] ?? path.basename(featureDir)).trim();
|
|
256
|
+
} catch {
|
|
257
|
+
return path.basename(featureDir);
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
appendRun(
|
|
261
|
+
runRecord({
|
|
262
|
+
state,
|
|
263
|
+
runStartedAt: now,
|
|
264
|
+
title,
|
|
265
|
+
agents: { builder: describeAgentProfile(config.agents.builder), reviewer: describeAgentProfile(config.agents.reviewer) },
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
246
270
|
let releaseSlot = () => {};
|
|
247
271
|
const shutdown = (signal: string) => {
|
|
248
272
|
log(`收到 ${signal},流水线停止`);
|
|
@@ -250,6 +274,7 @@ async function main(): Promise<number> {
|
|
|
250
274
|
state.stopReason = `流水线进程被 ${signal} 终止`;
|
|
251
275
|
state.queuedSince = null;
|
|
252
276
|
writeState(stateFile, state);
|
|
277
|
+
recordRun();
|
|
253
278
|
releaseSlot();
|
|
254
279
|
release();
|
|
255
280
|
process.exit(1);
|
|
@@ -331,6 +356,7 @@ async function main(): Promise<number> {
|
|
|
331
356
|
writeState(stateFile, state);
|
|
332
357
|
}
|
|
333
358
|
await runtime.close().catch(() => undefined);
|
|
359
|
+
recordRun();
|
|
334
360
|
releaseSlot();
|
|
335
361
|
release();
|
|
336
362
|
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Machine records under the kit home (1.9, ADR 0026): what this machine knows beyond one
|
|
2
|
+
// repository. Neither is committed anywhere; both are local to the machine.
|
|
3
|
+
//
|
|
4
|
+
// index/<key>.json one file per requirement that has run here: where it lives, so
|
|
5
|
+
// `req api list --all` and the Paseo plugin can find every requirement
|
|
6
|
+
// on the machine. The overview itself is still read live (ADR 0010).
|
|
7
|
+
// usage/runs.jsonl one line per pipeline run (a process from start to stop): steps,
|
|
8
|
+
// time, usage, gate and review rounds. What `req stats` reports on.
|
|
9
|
+
//
|
|
10
|
+
// Recording never affects a run: every write is best effort. REQ_KIT_NO_RECORD=1 turns both
|
|
11
|
+
// off (evaluation runs are not real requirements).
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { kitHome, kitVersion } from "./locations.js";
|
|
18
|
+
import type { PipelineState } from "./state.js";
|
|
19
|
+
|
|
20
|
+
export const RECORD_SCHEMA = 1;
|
|
21
|
+
|
|
22
|
+
function recordingOff(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
23
|
+
return env.REQ_KIT_NO_RECORD === "1" || env.REQ_KIT_NO_RECORD === "true";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function real(p: string): string {
|
|
27
|
+
try {
|
|
28
|
+
return fs.realpathSync(p);
|
|
29
|
+
} catch {
|
|
30
|
+
return path.resolve(p);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Index
|
|
36
|
+
|
|
37
|
+
export interface IndexEntry {
|
|
38
|
+
schema: 1;
|
|
39
|
+
/** Stable key of the requirement on this machine (also the file name). */
|
|
40
|
+
key: string;
|
|
41
|
+
/** Where the requirement is developed (its own worktree when it has one). */
|
|
42
|
+
repo: string;
|
|
43
|
+
featureDir: string;
|
|
44
|
+
/** The checkout the chat works in, when the requirement has its own worktree. */
|
|
45
|
+
sourceRepo: string | null;
|
|
46
|
+
workspaceId: string | null;
|
|
47
|
+
/** Name of the project: the source checkout's directory name. */
|
|
48
|
+
project: string;
|
|
49
|
+
registeredAt: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function indexDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
53
|
+
return path.join(kitHome(env), "index");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Key of a requirement: the checkout people work in plus the feature directory. */
|
|
57
|
+
export function requirementKey(sourceOrRepo: string, featureDir: string): string {
|
|
58
|
+
return createHash("sha1").update(`${real(sourceOrRepo)}\0${featureDir.split(path.sep).join("/")}`).digest("hex").slice(0, 16);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Records where this requirement lives, if it is not recorded that way already. Never throws. */
|
|
62
|
+
export function registerRequirement(state: Pick<PipelineState, "repo" | "featureDir" | "worktree">, env: NodeJS.ProcessEnv = process.env): void {
|
|
63
|
+
if (recordingOff(env)) return;
|
|
64
|
+
try {
|
|
65
|
+
const source = state.worktree?.sourceRepo ?? null;
|
|
66
|
+
const key = requirementKey(source ?? state.repo, state.featureDir);
|
|
67
|
+
const file = path.join(indexDir(env), `${key}.json`);
|
|
68
|
+
const existing = readEntry(file);
|
|
69
|
+
const entry: IndexEntry = {
|
|
70
|
+
schema: 1,
|
|
71
|
+
key,
|
|
72
|
+
repo: real(state.repo),
|
|
73
|
+
featureDir: state.featureDir.split(path.sep).join("/"),
|
|
74
|
+
sourceRepo: source ? real(source) : null,
|
|
75
|
+
workspaceId: state.worktree?.workspaceId ?? null,
|
|
76
|
+
project: path.basename(real(source ?? state.repo)),
|
|
77
|
+
registeredAt: existing?.registeredAt ?? new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(entry)) return;
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
82
|
+
fs.writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}\n`);
|
|
83
|
+
fs.renameSync(tmp, file);
|
|
84
|
+
} catch {
|
|
85
|
+
// The index is a convenience; the run goes on without it.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readEntry(file: string): IndexEntry | null {
|
|
90
|
+
try {
|
|
91
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as IndexEntry;
|
|
92
|
+
return raw && typeof raw.repo === "string" && typeof raw.featureDir === "string" ? raw : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every recorded requirement, with whether its directory is still there. */
|
|
99
|
+
export function readIndex(env: NodeJS.ProcessEnv = process.env): Array<IndexEntry & { present: boolean }> {
|
|
100
|
+
const dir = indexDir(env);
|
|
101
|
+
if (!fs.existsSync(dir)) return [];
|
|
102
|
+
return fs
|
|
103
|
+
.readdirSync(dir)
|
|
104
|
+
.filter((n) => n.endsWith(".json"))
|
|
105
|
+
.map((n) => readEntry(path.join(dir, n)))
|
|
106
|
+
.filter((e): e is IndexEntry => e !== null)
|
|
107
|
+
.map((e) => ({ ...e, present: fs.existsSync(path.join(e.repo, e.featureDir, ".pipeline", "state.json")) }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Removes the entries whose requirement is gone. Returns how many. */
|
|
111
|
+
export function pruneIndex(env: NodeJS.ProcessEnv = process.env): number {
|
|
112
|
+
let removed = 0;
|
|
113
|
+
for (const e of readIndex(env)) {
|
|
114
|
+
if (e.present) continue;
|
|
115
|
+
fs.rmSync(path.join(indexDir(env), `${e.key}.json`), { force: true });
|
|
116
|
+
removed++;
|
|
117
|
+
}
|
|
118
|
+
return removed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Usage records
|
|
123
|
+
|
|
124
|
+
export interface StepUsageRecord {
|
|
125
|
+
key: string;
|
|
126
|
+
stepId: string;
|
|
127
|
+
phase: string | null;
|
|
128
|
+
attempt: number;
|
|
129
|
+
round: number | null;
|
|
130
|
+
startedAt: string;
|
|
131
|
+
endedAt: string | null;
|
|
132
|
+
minutes: number;
|
|
133
|
+
outcome: string;
|
|
134
|
+
inputTokens: number | null;
|
|
135
|
+
outputTokens: number | null;
|
|
136
|
+
costUsd: number | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One pipeline run: a process from its start to its end (contracts/usage-record.md). */
|
|
140
|
+
export interface RunRecord {
|
|
141
|
+
schema: 1;
|
|
142
|
+
kind: "run";
|
|
143
|
+
machine: string;
|
|
144
|
+
kitVersion: string;
|
|
145
|
+
requirement: {
|
|
146
|
+
key: string;
|
|
147
|
+
project: string;
|
|
148
|
+
name: string;
|
|
149
|
+
title: string;
|
|
150
|
+
type: string | null;
|
|
151
|
+
profile: string;
|
|
152
|
+
};
|
|
153
|
+
run: {
|
|
154
|
+
startedAt: string;
|
|
155
|
+
endedAt: string;
|
|
156
|
+
attempt: number;
|
|
157
|
+
phase: string | null;
|
|
158
|
+
phaseMode: string | null;
|
|
159
|
+
/** State at the end of the run: completed, phase_done, waiting_for_approval, failed… */
|
|
160
|
+
status: string;
|
|
161
|
+
/** The report's verdict when the run completed (completed, completed_with_issues, not_completed). */
|
|
162
|
+
outcome: string | null;
|
|
163
|
+
stopReason: string | null;
|
|
164
|
+
};
|
|
165
|
+
agents: { builder: string | null; reviewer: string | null };
|
|
166
|
+
steps: StepUsageRecord[];
|
|
167
|
+
gate: { runs: number; failed: number };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function usageDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
171
|
+
return path.join(kitHome(env), "usage");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function runsFile(env: NodeJS.ProcessEnv = process.env): string {
|
|
175
|
+
return path.join(usageDir(env), "runs.jsonl");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function minutesBetween(from: string, to: string | null, now: Date): number {
|
|
179
|
+
const end = to ? new Date(to) : now;
|
|
180
|
+
return Math.max(0, Math.round(((end.getTime() - new Date(from).getTime()) / 60_000) * 10) / 10);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The record of the run that started at `runStartedAt`, from the state at its end. */
|
|
184
|
+
export function runRecord(input: {
|
|
185
|
+
state: PipelineState;
|
|
186
|
+
runStartedAt: string;
|
|
187
|
+
title: string;
|
|
188
|
+
agents: { builder: string | null; reviewer: string | null };
|
|
189
|
+
now?: Date;
|
|
190
|
+
machine?: string;
|
|
191
|
+
}): RunRecord {
|
|
192
|
+
const { state } = input;
|
|
193
|
+
const now = input.now ?? new Date();
|
|
194
|
+
const source = state.worktree?.sourceRepo ?? state.repo;
|
|
195
|
+
const since = new Date(input.runStartedAt).getTime();
|
|
196
|
+
const steps = state.history
|
|
197
|
+
.filter((h) => new Date(h.startedAt).getTime() >= since)
|
|
198
|
+
.map(
|
|
199
|
+
(h): StepUsageRecord => ({
|
|
200
|
+
key: h.key,
|
|
201
|
+
stepId: h.stepId,
|
|
202
|
+
phase: h.phase ?? null,
|
|
203
|
+
attempt: h.attempt,
|
|
204
|
+
round: h.round,
|
|
205
|
+
startedAt: h.startedAt,
|
|
206
|
+
endedAt: h.endedAt,
|
|
207
|
+
minutes: minutesBetween(h.startedAt, h.endedAt, now),
|
|
208
|
+
outcome: h.outcome,
|
|
209
|
+
inputTokens: h.usage?.inputTokens ?? null,
|
|
210
|
+
outputTokens: h.usage?.outputTokens ?? null,
|
|
211
|
+
costUsd: h.usage?.totalCostUsd ?? null,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
const gateRuns = [...(state.gate?.runs ?? []), ...(state.joint ?? []).flatMap((j) => j.gate?.runs ?? [])].filter(
|
|
215
|
+
(r) => new Date(r.at).getTime() >= since,
|
|
216
|
+
);
|
|
217
|
+
return {
|
|
218
|
+
schema: 1,
|
|
219
|
+
kind: "run",
|
|
220
|
+
machine: input.machine ?? os.hostname(),
|
|
221
|
+
kitVersion: kitVersion(),
|
|
222
|
+
requirement: {
|
|
223
|
+
key: requirementKey(source, state.featureDir),
|
|
224
|
+
project: path.basename(real(source)),
|
|
225
|
+
name: path.basename(state.featureDir),
|
|
226
|
+
title: input.title,
|
|
227
|
+
type: state.type ?? null,
|
|
228
|
+
profile: state.profile,
|
|
229
|
+
},
|
|
230
|
+
run: {
|
|
231
|
+
startedAt: input.runStartedAt,
|
|
232
|
+
endedAt: now.toISOString(),
|
|
233
|
+
attempt: state.attempt,
|
|
234
|
+
phase: state.phase ?? null,
|
|
235
|
+
phaseMode: state.phases?.mode ?? null,
|
|
236
|
+
status: state.status,
|
|
237
|
+
outcome: state.status === "completed" ? state.outcome : null,
|
|
238
|
+
stopReason: state.stopReason,
|
|
239
|
+
},
|
|
240
|
+
agents: input.agents,
|
|
241
|
+
steps,
|
|
242
|
+
gate: { runs: gateRuns.length, failed: gateRuns.filter((r) => !r.passed).length },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Appends a run record. Never throws. */
|
|
247
|
+
export function appendRun(record: RunRecord, env: NodeJS.ProcessEnv = process.env): void {
|
|
248
|
+
if (recordingOff(env)) return;
|
|
249
|
+
try {
|
|
250
|
+
fs.mkdirSync(usageDir(env), { recursive: true });
|
|
251
|
+
fs.appendFileSync(runsFile(env), `${JSON.stringify(record)}\n`);
|
|
252
|
+
} catch {
|
|
253
|
+
// Usage records are a convenience; the run is not affected.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Run records from a JSONL file (this machine's, or an export from another). Bad lines are skipped. */
|
|
258
|
+
export function readRuns(file: string): RunRecord[] {
|
|
259
|
+
if (!fs.existsSync(file)) return [];
|
|
260
|
+
const out: RunRecord[] = [];
|
|
261
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
262
|
+
if (!line.trim()) continue;
|
|
263
|
+
try {
|
|
264
|
+
const r = JSON.parse(line) as RunRecord;
|
|
265
|
+
if (r && r.kind === "run" && r.schema === RECORD_SCHEMA && r.requirement && r.run) out.push(r);
|
|
266
|
+
} catch {
|
|
267
|
+
// A torn last line (power loss mid-write) or a foreign line.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
package/engine/src/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { DefectHandling, PhaseMode, ProfileName, RequirementType, VerifyEnvironment } from "./config.js";
|
|
6
|
+
import { registerRequirement } from "./records.js";
|
|
6
7
|
import type { AgentUsage } from "./usage.js";
|
|
7
8
|
import type { StepId } from "./steps.js";
|
|
8
9
|
|
|
@@ -246,6 +247,8 @@ export function writeState(file: string, state: PipelineState): void {
|
|
|
246
247
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
247
248
|
fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
248
249
|
fs.renameSync(tmp, file);
|
|
250
|
+
// The machine's index of requirements (1.9): written once, and again only if it moved.
|
|
251
|
+
registerRequirement(state);
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
export function hashFile(file: string): string {
|