@intentius/chant 0.111.0 → 0.112.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/dist/op/steward.d.ts +1 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +4 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/steward-index.d.ts +89 -0
- package/dist/workspace/steward-index.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/op/steward.ts +1 -1
- package/src/workspace/status-stewards.ts +23 -35
- package/src/workspace/steward-index.test.ts +171 -0
- package/src/workspace/steward-index.ts +226 -0
package/dist/op/steward.d.ts
CHANGED
|
@@ -208,7 +208,7 @@ export declare function readinessKeys(result: unknown): {
|
|
|
208
208
|
keys: string[];
|
|
209
209
|
} | null;
|
|
210
210
|
/** The form a steward takes in `env` (`local` when none is named). */
|
|
211
|
-
export declare function stewardFormFor(steward: StewardDeclaration, env?: string): StewardForm;
|
|
211
|
+
export declare function stewardFormFor(steward: Pick<StewardDeclaration, "form">, env?: string): StewardForm;
|
|
212
212
|
/**
|
|
213
213
|
* The lease a local steward holds for as long as it runs, so a second
|
|
214
214
|
* `chant operator --steward` for the same steward is refused. It sits under
|
package/dist/op/steward.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steward.d.ts","sourceRoot":"","sources":["../../src/op/steward.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"steward.d.ts","sourceRoot":"","sources":["../../src/op/steward.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* its Ops with their schedules, and each Op's last run.
|
|
5
5
|
*
|
|
6
6
|
* The steward itself is read from its declaration, the same `*.op.ts` files
|
|
7
|
-
* `chant operator --steward` reads (`../op/discover.ts`),
|
|
7
|
+
* `chant operator --steward` reads (`../op/discover.ts`), through the index
|
|
8
|
+
* kept per tree (`./steward-index.ts`, #3636), and only for a member
|
|
8
9
|
* of kind `chant` that is a chant project of its own (a `chant.config.ts` or
|
|
9
10
|
* `.json` in its directory), so a member never lists a parent project's
|
|
10
11
|
* steward. Everything
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
*/
|
|
47
48
|
import { type StewardForm } from "../op/steward.js";
|
|
48
49
|
import { type ConvergeTickRecord } from "../lifecycle/converge-ledger.js";
|
|
50
|
+
import { type StewardIndex } from "./steward-index.js";
|
|
49
51
|
import type { OpRunRecord } from "../op/runtime.js";
|
|
50
52
|
/** Why a member's stewards can't be fully listed. Closed: a new code is a contract change. */
|
|
51
53
|
export declare const STEWARD_REASON_CODES: readonly ["stewards-unreadable", "stewards-conflict", "steward-runs-unreadable"];
|
|
@@ -264,5 +266,6 @@ export declare function readMemberStewards(memberDir: string, env: string, now:
|
|
|
264
266
|
}[];
|
|
265
267
|
} | null, deps?: {
|
|
266
268
|
readQuestions?: ReadQuestionStates;
|
|
269
|
+
readIndex?: (memberDir: string) => Promise<StewardIndex>;
|
|
267
270
|
}): Promise<MemberStewards>;
|
|
268
271
|
//# sourceMappingURL=status-stewards.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-stewards.d.ts","sourceRoot":"","sources":["../../src/workspace/status-stewards.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"status-stewards.d.ts","sourceRoot":"","sources":["../../src/workspace/status-stewards.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `chant workspace status` reads of a project's stewards and Ops
|
|
3
|
+
* (#3636), kept per tree so a status doesn't import every `*.op.ts` file
|
|
4
|
+
* each time it runs.
|
|
5
|
+
*
|
|
6
|
+
* Discovery (`../op/discover.ts`) has to import the Op modules, and an Op
|
|
7
|
+
* module may do work when it is imported: run git, read files, even start
|
|
8
|
+
* another chant. Status needs only plain fields of what it finds: each
|
|
9
|
+
* steward's name, file, form, vault, capabilities and Ops, and of each Op
|
|
10
|
+
* its name, schedule, labels, work lease kind and `changesCheckout`. The
|
|
11
|
+
* first status on a tree discovers them and writes that index to
|
|
12
|
+
* `<cache dir>/workspace-stewards/<checkout>/<key>.json`; later ones read
|
|
13
|
+
* it back, until the tree changes. The cache dir is the one the graph cache
|
|
14
|
+
* uses (`./graph-cache.ts`): `$CHANT_CACHE_DIR`, else
|
|
15
|
+
* `$XDG_CACHE_HOME/chant`, else `~/.cache/chant`, outside the workspace,
|
|
16
|
+
* since a read never changes the workspace it reads.
|
|
17
|
+
*
|
|
18
|
+
* The tree is the checkout's `HEAD` tree plus every changed or untracked
|
|
19
|
+
* file `git status` lists (its path and contents), and the chant version,
|
|
20
|
+
* so a commit, an edit or a new file anywhere in the checkout discovers
|
|
21
|
+
* again. Files git ignores (`node_modules`) are not part of it. An index is
|
|
22
|
+
* only written when every Op file imported, so an import that failed is
|
|
23
|
+
* tried again by the next status. `CHANT_STEWARD_INDEX=0` turns the index
|
|
24
|
+
* off, for an Op module whose declaration depends on more than its tree.
|
|
25
|
+
*/
|
|
26
|
+
import { type StewardForm } from "../op/steward.js";
|
|
27
|
+
/** What status reads of an Op. */
|
|
28
|
+
export interface IndexedOp {
|
|
29
|
+
name: string;
|
|
30
|
+
schedule: {
|
|
31
|
+
cron: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/** Absent when the Op has none. */
|
|
34
|
+
labels?: Record<string, string>;
|
|
35
|
+
workLease: {
|
|
36
|
+
kind: string | null;
|
|
37
|
+
} | null;
|
|
38
|
+
changesCheckout: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** What status reads of a steward. */
|
|
41
|
+
export interface IndexedSteward {
|
|
42
|
+
name: string;
|
|
43
|
+
/** The `*.op.ts` file declaring it, absolute. */
|
|
44
|
+
filePath: string;
|
|
45
|
+
/** Every Op it runs: its turns' Ops, then those beside them. */
|
|
46
|
+
ops: IndexedOp[];
|
|
47
|
+
/** The Ops it runs beside its turns (#2861), and whether each declares a ready step. */
|
|
48
|
+
beside: {
|
|
49
|
+
op: string;
|
|
50
|
+
ready: boolean;
|
|
51
|
+
}[];
|
|
52
|
+
form: {
|
|
53
|
+
default: StewardForm;
|
|
54
|
+
environments: Record<string, StewardForm>;
|
|
55
|
+
};
|
|
56
|
+
capabilities: string[];
|
|
57
|
+
vault: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface StewardIndex {
|
|
60
|
+
/** In discovery's order. */
|
|
61
|
+
stewards: IndexedSteward[];
|
|
62
|
+
/** Files that could not be imported. */
|
|
63
|
+
errors: string[];
|
|
64
|
+
/** Stewards dropped as second writers. */
|
|
65
|
+
conflicts: string[];
|
|
66
|
+
/** The project's Ops that no steward lists, or null when they couldn't be discovered. */
|
|
67
|
+
otherOps: IndexedOp[] | null;
|
|
68
|
+
}
|
|
69
|
+
/** Discover the stewards and Ops under `memberDir`, importing every Op file. Throws as `discoverStewards` does. */
|
|
70
|
+
export declare function discoverStewardIndex(memberDir: string): Promise<StewardIndex>;
|
|
71
|
+
/**
|
|
72
|
+
* The key of the tree `memberDir` is in: its `HEAD` tree, each changed or
|
|
73
|
+
* untracked file's path and contents, the chant version and `memberDir`.
|
|
74
|
+
* Null outside a checkout, or when git can't say.
|
|
75
|
+
*/
|
|
76
|
+
export declare function treeKey(memberDir: string): Promise<{
|
|
77
|
+
key: string;
|
|
78
|
+
dir: string;
|
|
79
|
+
checkout: string;
|
|
80
|
+
} | null>;
|
|
81
|
+
/**
|
|
82
|
+
* The stewards and Ops under `memberDir`: from the index for its tree when
|
|
83
|
+
* there is one, else discovered and indexed. Throws as `discoverStewards`
|
|
84
|
+
* does.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readStewardIndex(memberDir: string, deps?: {
|
|
87
|
+
discover?: typeof discoverStewardIndex;
|
|
88
|
+
}): Promise<StewardIndex>;
|
|
89
|
+
//# sourceMappingURL=steward-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steward-index.d.ts","sourceRoot":"","sources":["../../src/workspace/steward-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAUH,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAuBlE,kCAAkC;AAClC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,GAAG,EAAE,SAAS,EAAE,CAAC;IACjB,wFAAwF;IACxF,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IACzC,IAAI,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;KAAE,CAAC;IAC1E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,4BAA4B;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,wCAAwC;IACxC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,yFAAyF;IACzF,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;CAC9B;AAYD,mHAAmH;AACnH,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsBnF;AASD;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAyB/G;AA8BD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,oBAAoB,CAAA;CAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CA0BtI"}
|
package/package.json
CHANGED
package/src/op/steward.ts
CHANGED
|
@@ -352,7 +352,7 @@ export function readinessKeys(result: unknown): { ready: boolean; keys: string[]
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
/** The form a steward takes in `env` (`local` when none is named). */
|
|
355
|
-
export function stewardFormFor(steward: StewardDeclaration, env: string = DEFAULT_STEWARD_ENV): StewardForm {
|
|
355
|
+
export function stewardFormFor(steward: Pick<StewardDeclaration, "form">, env: string = DEFAULT_STEWARD_ENV): StewardForm {
|
|
356
356
|
return steward.form.environments[env] ?? steward.form.default;
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* its Ops with their schedules, and each Op's last run.
|
|
5
5
|
*
|
|
6
6
|
* The steward itself is read from its declaration, the same `*.op.ts` files
|
|
7
|
-
* `chant operator --steward` reads (`../op/discover.ts`),
|
|
7
|
+
* `chant operator --steward` reads (`../op/discover.ts`), through the index
|
|
8
|
+
* kept per tree (`./steward-index.ts`, #3636), and only for a member
|
|
8
9
|
* of kind `chant` that is a chant project of its own (a `chant.config.ts` or
|
|
9
10
|
* `.json` in its directory), so a member never lists a parent project's
|
|
10
11
|
* steward. Everything
|
|
@@ -47,8 +48,7 @@
|
|
|
47
48
|
|
|
48
49
|
import { existsSync, realpathSync } from "node:fs";
|
|
49
50
|
import { dirname, join, relative, resolve } from "node:path";
|
|
50
|
-
import {
|
|
51
|
-
import { stewardBesideFor, stewardFormFor, stewardLeaseName, type StewardForm } from "../op/steward";
|
|
51
|
+
import { stewardFormFor, stewardLeaseName, type StewardForm } from "../op/steward";
|
|
52
52
|
import { readRunLedger, runEnvOf } from "../lifecycle/run-ledger";
|
|
53
53
|
import { readConvergeLedger, type ConvergeTickRecord } from "../lifecycle/converge-ledger";
|
|
54
54
|
import { leaseRef } from "../lifecycle/lease";
|
|
@@ -56,7 +56,7 @@ import { readBlobBySha, readRefSha } from "../lifecycle/git";
|
|
|
56
56
|
import { resolveMemberLedger } from "../lifecycle/member-ledger";
|
|
57
57
|
import { listWorkLeases } from "../lifecycle/work-lease";
|
|
58
58
|
import { stewardWorkHolder } from "../op/work-lease-run";
|
|
59
|
-
import type
|
|
59
|
+
import { readStewardIndex, type IndexedOp, type IndexedSteward, type StewardIndex } from "./steward-index";
|
|
60
60
|
import type { OpRunRecord } from "../op/runtime";
|
|
61
61
|
import { readInFlightRun } from "../op/run-live";
|
|
62
62
|
import { approveCommand } from "./status-gates";
|
|
@@ -342,19 +342,14 @@ async function readLeaseRef(leaseName: string, memberDir: string, now: string):
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
/** An Op's `beside` entry (#2861): null for an Op run as one of the steward's turns. */
|
|
345
|
-
async function besideOf(
|
|
346
|
-
|
|
347
|
-
op: string,
|
|
348
|
-
memberDir: string,
|
|
349
|
-
now: string,
|
|
350
|
-
): Promise<StatusStewardOp["beside"]> {
|
|
351
|
-
const beside = stewardBesideFor(declaration, op);
|
|
345
|
+
async function besideOf(steward: IndexedSteward, op: string, memberDir: string, now: string): Promise<StatusStewardOp["beside"]> {
|
|
346
|
+
const beside = steward.beside.find((b) => b.op === op);
|
|
352
347
|
if (!beside) return null;
|
|
353
|
-
return { ready: beside.ready
|
|
348
|
+
return { ready: beside.ready, lease: await readLeaseRef(op, memberDir, now) };
|
|
354
349
|
}
|
|
355
350
|
|
|
356
351
|
/** The work leases `steward`'s turns of `op` hold, from the ledger of the Op's kind or the member's. */
|
|
357
|
-
async function readHeldWorkLeases(steward: string, op:
|
|
352
|
+
async function readHeldWorkLeases(steward: string, op: IndexedOp, memberDir: string, now: string): Promise<StatusStewardWorkLease[]> {
|
|
358
353
|
try {
|
|
359
354
|
const kind = op.workLease?.kind;
|
|
360
355
|
const cwd = kind ? dirname(resolve(memberDir, kind)) : memberDir;
|
|
@@ -378,7 +373,7 @@ export async function readMemberStewards(
|
|
|
378
373
|
now: string,
|
|
379
374
|
kind = "chant",
|
|
380
375
|
box: { capabilities: { name: string; broker: string | null }[] } | null = null,
|
|
381
|
-
deps: { readQuestions?: ReadQuestionStates } = {},
|
|
376
|
+
deps: { readQuestions?: ReadQuestionStates; readIndex?: (memberDir: string) => Promise<StewardIndex> } = {},
|
|
382
377
|
): Promise<MemberStewards> {
|
|
383
378
|
const reasons: MemberStewards["reasons"] = [];
|
|
384
379
|
// Read once per member, and only when a run stopped on a question.
|
|
@@ -386,19 +381,18 @@ export async function readMemberStewards(
|
|
|
386
381
|
const questionStates = () => (states ??= (deps.readQuestions ?? readQuestionStates)(memberDir));
|
|
387
382
|
if (kind !== "chant" || !isChantProject(memberDir)) return { stewards: [], reasons };
|
|
388
383
|
|
|
389
|
-
let
|
|
384
|
+
let index: StewardIndex;
|
|
390
385
|
try {
|
|
391
|
-
|
|
386
|
+
index = await (deps.readIndex ?? readStewardIndex)(memberDir);
|
|
392
387
|
} catch (err) {
|
|
393
388
|
reasons.push({ code: "stewards-unreadable", message: err instanceof Error ? err.message.split("\n")[0] : String(err) });
|
|
394
389
|
return { stewards: [], reasons };
|
|
395
390
|
}
|
|
396
|
-
const {
|
|
397
|
-
for (const message of
|
|
398
|
-
for (const message of conflicts) reasons.push({ code: "stewards-conflict", message });
|
|
391
|
+
for (const message of index.errors) reasons.push({ code: "stewards-unreadable", message });
|
|
392
|
+
for (const message of index.conflicts) reasons.push({ code: "stewards-conflict", message });
|
|
399
393
|
|
|
400
394
|
const stewards: StatusSteward[] = [];
|
|
401
|
-
for (const
|
|
395
|
+
for (const declaration of [...index.stewards].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
402
396
|
const ops: StatusStewardOp[] = [];
|
|
403
397
|
const waiting: StatusSteward["waiting"] = [];
|
|
404
398
|
for (const op of declaration.ops) {
|
|
@@ -467,11 +461,11 @@ export async function readMemberStewards(
|
|
|
467
461
|
}
|
|
468
462
|
stewards.push({
|
|
469
463
|
name: declaration.name,
|
|
470
|
-
file: relative(realpathSync(memberDir), realpathSync(filePath)).split("\\").join("/"),
|
|
464
|
+
file: relative(realpathSync(memberDir), realpathSync(declaration.filePath)).split("\\").join("/"),
|
|
471
465
|
form: stewardFormFor(declaration, env),
|
|
472
466
|
forms: { default: declaration.form.default, environments: { ...declaration.form.environments } },
|
|
473
|
-
vault:
|
|
474
|
-
capabilities:
|
|
467
|
+
vault: declaration.vault,
|
|
468
|
+
capabilities: declaration.capabilities.map((name) => {
|
|
475
469
|
const declared = box?.capabilities.find((c) => c.name === name);
|
|
476
470
|
return { name, broker: declared?.broker ?? null, declared: declared !== undefined };
|
|
477
471
|
}),
|
|
@@ -480,7 +474,7 @@ export async function readMemberStewards(
|
|
|
480
474
|
waiting,
|
|
481
475
|
});
|
|
482
476
|
}
|
|
483
|
-
if (stewards.length > 0) await addUndeclaredWaits(memberDir, stewards,
|
|
477
|
+
if (stewards.length > 0 && index.otherOps) await addUndeclaredWaits(memberDir, stewards, index.otherOps, reasons, now, questionStates);
|
|
484
478
|
return { stewards, reasons };
|
|
485
479
|
}
|
|
486
480
|
|
|
@@ -489,26 +483,20 @@ export async function readMemberStewards(
|
|
|
489
483
|
* steward its record names. A steward's process can start an Op itself
|
|
490
484
|
* with `CHANT_STEWARD` set (studio#137), and the run ledger then records
|
|
491
485
|
* the run as the steward's though the declaration does not list the Op.
|
|
492
|
-
*
|
|
493
|
-
*
|
|
486
|
+
* `ops` are the member's Ops no steward lists, from the index; import
|
|
487
|
+
* failures are left out there, as discovering the stewards reported them as
|
|
488
|
+
* `stewards-unreadable`.
|
|
494
489
|
*/
|
|
495
490
|
async function addUndeclaredWaits(
|
|
496
491
|
memberDir: string,
|
|
497
492
|
stewards: StatusSteward[],
|
|
498
|
-
|
|
493
|
+
ops: IndexedOp[],
|
|
499
494
|
reasons: MemberStewards["reasons"],
|
|
500
495
|
now: string,
|
|
501
496
|
questionStates: () => Promise<Map<string, string> | null>,
|
|
502
497
|
): Promise<void> {
|
|
503
|
-
const declared = new Set([...found.values()].flatMap(({ declaration }) => declaration.ops.map((op) => op.name)));
|
|
504
|
-
let ops: OpConfig[];
|
|
505
|
-
try {
|
|
506
|
-
ops = [...(await discoverOps({ cwd: memberDir })).ops.values()].map((d) => d.config).filter((op) => !declared.has(op.name));
|
|
507
|
-
} catch {
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
498
|
const byName = new Map(stewards.map((s) => [s.name, s]));
|
|
511
|
-
for (const op of ops.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
499
|
+
for (const op of [...ops].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
512
500
|
const opEnv = runEnvOf(op);
|
|
513
501
|
try {
|
|
514
502
|
const newest = (await readRunLedger(opEnv, op.name, { cwd: memberDir })).records.at(-1);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steward index behind `chant workspace status` (#3636): discovered
|
|
3
|
+
* once per tree, read back while the tree is the same, discovered again
|
|
4
|
+
* after an edit, a new file or a commit, and never written into the
|
|
5
|
+
* checkout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { cleanScratch, commitAll, git, repo, scratchDir } from "./__fixtures__/contract-repo";
|
|
12
|
+
import { declareSteward } from "../op/steward";
|
|
13
|
+
import type { OpConfig } from "../op/types";
|
|
14
|
+
import { discoverStewardIndex, readStewardIndex, type StewardIndex } from "./steward-index";
|
|
15
|
+
import { readMemberStewards } from "./status-stewards";
|
|
16
|
+
|
|
17
|
+
afterAll(cleanScratch);
|
|
18
|
+
|
|
19
|
+
let cache: string;
|
|
20
|
+
let before: string | undefined;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
before = process.env.CHANT_CACHE_DIR;
|
|
23
|
+
cache = scratchDir("chant-steward-index-cache-");
|
|
24
|
+
process.env.CHANT_CACHE_DIR = cache;
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (before === undefined) delete process.env.CHANT_CACHE_DIR;
|
|
28
|
+
else process.env.CHANT_CACHE_DIR = before;
|
|
29
|
+
delete process.env.CHANT_STEWARD_INDEX;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function op(name: string, extra: Partial<OpConfig> = {}): OpConfig {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
overview: `${name} fixture`,
|
|
36
|
+
phases: [{ name: "only", steps: [{ kind: "activity", fn: "noop", args: {} }] }],
|
|
37
|
+
...extra,
|
|
38
|
+
} as OpConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A chant project whose one Op file declares a steward and exports another Op it doesn't list. */
|
|
42
|
+
function project(): string {
|
|
43
|
+
const tick = op("tick", { schedule: { cron: "0 * * * *" }, labels: { Env: "box" } });
|
|
44
|
+
const build = op("build", { workLease: { item: "W-001", kind: "work/work.kind.mjs" }, changesCheckout: true });
|
|
45
|
+
const steward = declareSteward({
|
|
46
|
+
name: "box-steward",
|
|
47
|
+
ops: [tick],
|
|
48
|
+
beside: [{ op: build, ready: { kind: "activity", fn: "ready", args: {} } }],
|
|
49
|
+
form: { default: "local", environments: { k3d: "fountain" } },
|
|
50
|
+
capabilities: ["inference"],
|
|
51
|
+
});
|
|
52
|
+
return repo(
|
|
53
|
+
{
|
|
54
|
+
"chant.config.json": "{}\n",
|
|
55
|
+
"ops/steward.op.ts": `export const steward = ${JSON.stringify(steward)};\nexport const lonely = { props: ${JSON.stringify(op("lonely"))} };\n`,
|
|
56
|
+
},
|
|
57
|
+
true,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `readStewardIndex`, counting the discoveries it makes. */
|
|
62
|
+
function counted(dir: string) {
|
|
63
|
+
let n = 0;
|
|
64
|
+
const discover = async (d: string): Promise<StewardIndex> => {
|
|
65
|
+
n++;
|
|
66
|
+
return discoverStewardIndex(d);
|
|
67
|
+
};
|
|
68
|
+
return { read: () => readStewardIndex(dir, { discover }), discoveries: () => n };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("the steward index (#3636)", () => {
|
|
72
|
+
test("holds what status reads of the stewards and Ops", async () => {
|
|
73
|
+
const root = project();
|
|
74
|
+
const index = await discoverStewardIndex(root);
|
|
75
|
+
expect(index.errors).toEqual([]);
|
|
76
|
+
expect(index.conflicts).toEqual([]);
|
|
77
|
+
expect(index.stewards).toHaveLength(1);
|
|
78
|
+
const [s] = index.stewards;
|
|
79
|
+
expect(s).toMatchObject({
|
|
80
|
+
name: "box-steward",
|
|
81
|
+
beside: [{ op: "build", ready: true }],
|
|
82
|
+
form: { default: "local", environments: { k3d: "fountain" } },
|
|
83
|
+
capabilities: ["inference"],
|
|
84
|
+
vault: null,
|
|
85
|
+
});
|
|
86
|
+
expect(s.filePath.endsWith(join("ops", "steward.op.ts"))).toBe(true);
|
|
87
|
+
expect(s.ops).toEqual([
|
|
88
|
+
{ name: "tick", schedule: { cron: "0 * * * *" }, labels: { Env: "box" }, workLease: null, changesCheckout: false },
|
|
89
|
+
{ name: "build", schedule: null, workLease: { kind: "work/work.kind.mjs" }, changesCheckout: true },
|
|
90
|
+
]);
|
|
91
|
+
expect(index.otherOps).toEqual([{ name: "lonely", schedule: null, workLease: null, changesCheckout: false }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a second read of the same tree discovers nothing", async () => {
|
|
95
|
+
const root = project();
|
|
96
|
+
const c = counted(root);
|
|
97
|
+
const first = await c.read();
|
|
98
|
+
const second = await c.read();
|
|
99
|
+
expect(c.discoveries()).toBe(1);
|
|
100
|
+
expect(second).toEqual(JSON.parse(JSON.stringify(first)));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an edit, a new file or a commit discovers again", async () => {
|
|
104
|
+
const root = project();
|
|
105
|
+
const c = counted(root);
|
|
106
|
+
await c.read();
|
|
107
|
+
// A module already imported in this process isn't read again, so only the count is checked.
|
|
108
|
+
writeFileSync(join(root, "ops", "steward.op.ts"), "export const nothing = 1;\n");
|
|
109
|
+
await c.read();
|
|
110
|
+
expect(c.discoveries()).toBe(2);
|
|
111
|
+
await c.read();
|
|
112
|
+
expect(c.discoveries()).toBe(2);
|
|
113
|
+
mkdirSync(join(root, "lib"));
|
|
114
|
+
writeFileSync(join(root, "lib", "helper.ts"), "export const x = 1;\n");
|
|
115
|
+
await c.read();
|
|
116
|
+
expect(c.discoveries()).toBe(3);
|
|
117
|
+
commitAll(root);
|
|
118
|
+
await c.read();
|
|
119
|
+
expect(c.discoveries()).toBe(4);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("an Op file that fails to import is not indexed, so the next read tries again", async () => {
|
|
123
|
+
const root = project();
|
|
124
|
+
writeFileSync(join(root, "ops", "broken.op.ts"), "throw new Error('not today');\n");
|
|
125
|
+
const c = counted(root);
|
|
126
|
+
expect((await c.read()).errors).toHaveLength(1);
|
|
127
|
+
expect((await c.read()).errors).toHaveLength(1);
|
|
128
|
+
expect(c.discoveries()).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("CHANT_STEWARD_INDEX=0 discovers every time", async () => {
|
|
132
|
+
const root = project();
|
|
133
|
+
process.env.CHANT_STEWARD_INDEX = "0";
|
|
134
|
+
const c = counted(root);
|
|
135
|
+
await c.read();
|
|
136
|
+
await c.read();
|
|
137
|
+
expect(c.discoveries()).toBe(2);
|
|
138
|
+
expect(existsSync(join(cache, "workspace-stewards"))).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the index lives in the cache dir, never in the checkout", async () => {
|
|
142
|
+
const root = project();
|
|
143
|
+
const status = git(root, "status", "--porcelain", "--ignored");
|
|
144
|
+
await readStewardIndex(root);
|
|
145
|
+
expect(git(root, "status", "--porcelain", "--ignored")).toBe(status);
|
|
146
|
+
const [dir] = readdirSync(join(cache, "workspace-stewards"));
|
|
147
|
+
expect(readdirSync(join(cache, "workspace-stewards", dir)).filter((f) => f.endsWith(".json"))).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("the indexes of a checkout that is gone go on the next write", async () => {
|
|
151
|
+
const gone = project();
|
|
152
|
+
await readStewardIndex(gone);
|
|
153
|
+
rmSync(gone, { recursive: true, force: true });
|
|
154
|
+
await readStewardIndex(project());
|
|
155
|
+
expect(readdirSync(join(cache, "workspace-stewards"))).toHaveLength(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("status lists the same stewards from the index as from discovery", async () => {
|
|
159
|
+
const root = project();
|
|
160
|
+
const now = "2027-01-01T00:00:00Z";
|
|
161
|
+
process.env.CHANT_STEWARD_INDEX = "0";
|
|
162
|
+
const discovered = await readMemberStewards(root, "k3d", now);
|
|
163
|
+
delete process.env.CHANT_STEWARD_INDEX;
|
|
164
|
+
await readMemberStewards(root, "k3d", now);
|
|
165
|
+
const indexed = await readMemberStewards(root, "k3d", now, "chant", null, {
|
|
166
|
+
readIndex: (d) => readStewardIndex(d, { discover: () => Promise.reject(new Error("should have been read from the index")) }),
|
|
167
|
+
});
|
|
168
|
+
expect(indexed).toEqual(discovered);
|
|
169
|
+
expect(indexed.stewards[0]).toMatchObject({ name: "box-steward", form: "fountain", file: "ops/steward.op.ts" });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `chant workspace status` reads of a project's stewards and Ops
|
|
3
|
+
* (#3636), kept per tree so a status doesn't import every `*.op.ts` file
|
|
4
|
+
* each time it runs.
|
|
5
|
+
*
|
|
6
|
+
* Discovery (`../op/discover.ts`) has to import the Op modules, and an Op
|
|
7
|
+
* module may do work when it is imported: run git, read files, even start
|
|
8
|
+
* another chant. Status needs only plain fields of what it finds: each
|
|
9
|
+
* steward's name, file, form, vault, capabilities and Ops, and of each Op
|
|
10
|
+
* its name, schedule, labels, work lease kind and `changesCheckout`. The
|
|
11
|
+
* first status on a tree discovers them and writes that index to
|
|
12
|
+
* `<cache dir>/workspace-stewards/<checkout>/<key>.json`; later ones read
|
|
13
|
+
* it back, until the tree changes. The cache dir is the one the graph cache
|
|
14
|
+
* uses (`./graph-cache.ts`): `$CHANT_CACHE_DIR`, else
|
|
15
|
+
* `$XDG_CACHE_HOME/chant`, else `~/.cache/chant`, outside the workspace,
|
|
16
|
+
* since a read never changes the workspace it reads.
|
|
17
|
+
*
|
|
18
|
+
* The tree is the checkout's `HEAD` tree plus every changed or untracked
|
|
19
|
+
* file `git status` lists (its path and contents), and the chant version,
|
|
20
|
+
* so a commit, an edit or a new file anywhere in the checkout discovers
|
|
21
|
+
* again. Files git ignores (`node_modules`) are not part of it. An index is
|
|
22
|
+
* only written when every Op file imported, so an import that failed is
|
|
23
|
+
* tried again by the next status. `CHANT_STEWARD_INDEX=0` turns the index
|
|
24
|
+
* off, for an Op module whose declaration depends on more than its tree.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { execFile } from "node:child_process";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { dirname, join, resolve } from "node:path";
|
|
32
|
+
import { promisify } from "node:util";
|
|
33
|
+
import { CHANT_VERSION } from "../cli/version";
|
|
34
|
+
import { discoverOps, discoverStewards } from "../op/discover";
|
|
35
|
+
import { stewardBesideOf, type StewardForm } from "../op/steward";
|
|
36
|
+
import type { OpConfig } from "../op/types";
|
|
37
|
+
|
|
38
|
+
const execFileAsync = promisify(execFile);
|
|
39
|
+
|
|
40
|
+
/** Bumped when the index's shape changes, so an older index is never read. */
|
|
41
|
+
const INDEX_FORMAT = 1;
|
|
42
|
+
|
|
43
|
+
/** Where the indexes of the checkout at `top` live, and the checkout's real path. */
|
|
44
|
+
function indexDir(top: string, env: NodeJS.ProcessEnv = process.env): { dir: string; checkout: string } {
|
|
45
|
+
const base = env.CHANT_CACHE_DIR || join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "chant");
|
|
46
|
+
let checkout = resolve(top);
|
|
47
|
+
try {
|
|
48
|
+
checkout = realpathSync(checkout);
|
|
49
|
+
} catch {
|
|
50
|
+
// Keyed on the path as given.
|
|
51
|
+
}
|
|
52
|
+
return { dir: join(base, "workspace-stewards", createHash("sha256").update(checkout).digest("hex").slice(0, 16)), checkout };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** How many indexes a checkout keeps: the newest, by when they were written. */
|
|
56
|
+
const KEPT = 32;
|
|
57
|
+
|
|
58
|
+
/** What status reads of an Op. */
|
|
59
|
+
export interface IndexedOp {
|
|
60
|
+
name: string;
|
|
61
|
+
schedule: { cron: string } | null;
|
|
62
|
+
/** Absent when the Op has none. */
|
|
63
|
+
labels?: Record<string, string>;
|
|
64
|
+
workLease: { kind: string | null } | null;
|
|
65
|
+
changesCheckout: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What status reads of a steward. */
|
|
69
|
+
export interface IndexedSteward {
|
|
70
|
+
name: string;
|
|
71
|
+
/** The `*.op.ts` file declaring it, absolute. */
|
|
72
|
+
filePath: string;
|
|
73
|
+
/** Every Op it runs: its turns' Ops, then those beside them. */
|
|
74
|
+
ops: IndexedOp[];
|
|
75
|
+
/** The Ops it runs beside its turns (#2861), and whether each declares a ready step. */
|
|
76
|
+
beside: { op: string; ready: boolean }[];
|
|
77
|
+
form: { default: StewardForm; environments: Record<string, StewardForm> };
|
|
78
|
+
capabilities: string[];
|
|
79
|
+
vault: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StewardIndex {
|
|
83
|
+
/** In discovery's order. */
|
|
84
|
+
stewards: IndexedSteward[];
|
|
85
|
+
/** Files that could not be imported. */
|
|
86
|
+
errors: string[];
|
|
87
|
+
/** Stewards dropped as second writers. */
|
|
88
|
+
conflicts: string[];
|
|
89
|
+
/** The project's Ops that no steward lists, or null when they couldn't be discovered. */
|
|
90
|
+
otherOps: IndexedOp[] | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function indexOp(op: OpConfig): IndexedOp {
|
|
94
|
+
return {
|
|
95
|
+
name: op.name,
|
|
96
|
+
schedule: op.schedule ? { cron: op.schedule.cron } : null,
|
|
97
|
+
...(op.labels ? { labels: { ...op.labels } } : {}),
|
|
98
|
+
workLease: op.workLease ? { kind: op.workLease.kind ?? null } : null,
|
|
99
|
+
changesCheckout: op.changesCheckout === true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Discover the stewards and Ops under `memberDir`, importing every Op file. Throws as `discoverStewards` does. */
|
|
104
|
+
export async function discoverStewardIndex(memberDir: string): Promise<StewardIndex> {
|
|
105
|
+
const { stewards, errors, conflicts } = await discoverStewards({ cwd: memberDir });
|
|
106
|
+
const listed = [...stewards.values()].map(({ declaration, filePath }) => ({
|
|
107
|
+
name: declaration.name,
|
|
108
|
+
filePath,
|
|
109
|
+
ops: declaration.ops.map(indexOp),
|
|
110
|
+
beside: stewardBesideOf(declaration).map((b) => ({ op: b.op, ready: b.ready !== null && b.ready !== undefined })),
|
|
111
|
+
form: { default: declaration.form.default, environments: { ...declaration.form.environments } },
|
|
112
|
+
capabilities: Array.isArray(declaration.capabilities) ? [...declaration.capabilities] : [],
|
|
113
|
+
vault: typeof declaration.vault === "string" ? declaration.vault : null,
|
|
114
|
+
}));
|
|
115
|
+
let otherOps: IndexedOp[] | null = null;
|
|
116
|
+
// Only a project with a steward has waits to look for among its other Ops.
|
|
117
|
+
if (listed.length > 0) {
|
|
118
|
+
const declared = new Set(listed.flatMap((s) => s.ops.map((op) => op.name)));
|
|
119
|
+
try {
|
|
120
|
+
otherOps = [...(await discoverOps({ cwd: memberDir })).ops.values()].map((d) => indexOp(d.config)).filter((op) => !declared.has(op.name));
|
|
121
|
+
} catch {
|
|
122
|
+
otherOps = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { stewards: listed, errors, conflicts, otherOps };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function git(args: string[], cwd: string): Promise<string> {
|
|
129
|
+
// A read: `git status` must not take the index lock from a writer.
|
|
130
|
+
const env = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
131
|
+
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer: 64 * 1024 * 1024 });
|
|
132
|
+
return stdout;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The key of the tree `memberDir` is in: its `HEAD` tree, each changed or
|
|
137
|
+
* untracked file's path and contents, the chant version and `memberDir`.
|
|
138
|
+
* Null outside a checkout, or when git can't say.
|
|
139
|
+
*/
|
|
140
|
+
export async function treeKey(memberDir: string): Promise<{ key: string; dir: string; checkout: string } | null> {
|
|
141
|
+
try {
|
|
142
|
+
const top = (await git(["rev-parse", "--show-toplevel"], memberDir)).trim();
|
|
143
|
+
const head = (await git(["rev-parse", "--verify", "--quiet", "HEAD^{tree}"], top).catch(() => "")).trim();
|
|
144
|
+
const changed = (await git(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"], top))
|
|
145
|
+
.split("\0")
|
|
146
|
+
.filter((e) => e.length > 3)
|
|
147
|
+
.map((e) => e.slice(3))
|
|
148
|
+
.sort();
|
|
149
|
+
const h = createHash("sha256");
|
|
150
|
+
h.update(`${INDEX_FORMAT}\0${CHANT_VERSION}\0${resolve(memberDir)}\0${head}\0`);
|
|
151
|
+
for (const path of changed) {
|
|
152
|
+
h.update(`${path}\0`);
|
|
153
|
+
try {
|
|
154
|
+
const full = join(top, path);
|
|
155
|
+
h.update(statSync(full).isFile() ? readFileSync(full) : "dir");
|
|
156
|
+
} catch {
|
|
157
|
+
h.update("gone");
|
|
158
|
+
}
|
|
159
|
+
h.update("\0");
|
|
160
|
+
}
|
|
161
|
+
return { key: h.digest("hex"), ...indexDir(top) };
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Remove all but the {@link KEPT} newest indexes of this checkout, and the
|
|
169
|
+
* indexes of checkouts that are gone (each directory names its checkout in
|
|
170
|
+
* its `checkout` file).
|
|
171
|
+
*/
|
|
172
|
+
function prune(dir: string): void {
|
|
173
|
+
const files = readdirSync(dir)
|
|
174
|
+
.filter((f) => f.endsWith(".json"))
|
|
175
|
+
.map((f) => ({ f, at: statSync(join(dir, f)).mtimeMs }))
|
|
176
|
+
.sort((a, b) => b.at - a.at);
|
|
177
|
+
for (const { f } of files.slice(KEPT)) rmSync(join(dir, f), { force: true });
|
|
178
|
+
const parent = dirname(dir);
|
|
179
|
+
for (const other of readdirSync(parent)) {
|
|
180
|
+
const at = join(parent, other);
|
|
181
|
+
if (at === dir) continue;
|
|
182
|
+
try {
|
|
183
|
+
if (!existsSync(readFileSync(join(at, "checkout"), "utf-8"))) rmSync(at, { recursive: true, force: true });
|
|
184
|
+
} catch {
|
|
185
|
+
// No checkout file, or being written: left alone.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isIndex(v: unknown): v is StewardIndex {
|
|
191
|
+
const i = v as StewardIndex;
|
|
192
|
+
return !!i && Array.isArray(i.stewards) && Array.isArray(i.errors) && Array.isArray(i.conflicts) && (i.otherOps === null || Array.isArray(i.otherOps));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The stewards and Ops under `memberDir`: from the index for its tree when
|
|
197
|
+
* there is one, else discovered and indexed. Throws as `discoverStewards`
|
|
198
|
+
* does.
|
|
199
|
+
*/
|
|
200
|
+
export async function readStewardIndex(memberDir: string, deps: { discover?: typeof discoverStewardIndex } = {}): Promise<StewardIndex> {
|
|
201
|
+
const discover = deps.discover ?? discoverStewardIndex;
|
|
202
|
+
if (process.env.CHANT_STEWARD_INDEX === "0") return discover(memberDir);
|
|
203
|
+
const tree = await treeKey(memberDir);
|
|
204
|
+
if (!tree) return discover(memberDir);
|
|
205
|
+
const file = join(tree.dir, `${tree.key}.json`);
|
|
206
|
+
try {
|
|
207
|
+
const kept = JSON.parse(readFileSync(file, "utf-8")) as unknown;
|
|
208
|
+
if (isIndex(kept)) return kept;
|
|
209
|
+
} catch {
|
|
210
|
+
// None yet, or unreadable: discover.
|
|
211
|
+
}
|
|
212
|
+
const index = await discover(memberDir);
|
|
213
|
+
if (index.errors.length === 0) {
|
|
214
|
+
try {
|
|
215
|
+
mkdirSync(tree.dir, { recursive: true });
|
|
216
|
+
writeFileSync(join(tree.dir, "checkout"), tree.checkout);
|
|
217
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
218
|
+
writeFileSync(tmp, JSON.stringify(index));
|
|
219
|
+
renameSync(tmp, file);
|
|
220
|
+
prune(tree.dir);
|
|
221
|
+
} catch {
|
|
222
|
+
// A cache dir that can't be written: status still works, discovering each time.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return index;
|
|
226
|
+
}
|