team 0.1.1 → 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/README.md +38 -12
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +80 -8
- package/dist/approve/fingerprint.d.ts +29 -6
- package/dist/approve/fingerprint.js +57 -32
- package/dist/budgets/gate.d.ts +5 -4
- package/dist/budgets/gate.js +23 -14
- package/dist/budgets/readings.d.ts +57 -9
- package/dist/budgets/readings.js +134 -18
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +4 -2
- package/dist/budgets/table.js +29 -11
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +8 -5
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.js +18 -9
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +128 -8
- package/dist/commands/down.d.ts +6 -2
- package/dist/commands/down.js +40 -14
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +26 -4
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +22 -3
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -6
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +99 -19
- package/dist/conformance/adapter.d.ts +5 -0
- package/dist/conformance/adapter.js +44 -0
- package/dist/file/check.d.ts +20 -0
- package/dist/file/check.js +112 -0
- package/dist/file/lines.js +13 -2
- package/dist/file/sections/budgets.d.ts +15 -0
- package/dist/file/sections/budgets.js +180 -0
- package/dist/file/sections/coordinator.d.ts +2 -0
- package/dist/file/sections/coordinator.js +11 -0
- package/dist/file/sections/format.d.ts +2 -0
- package/dist/file/sections/format.js +13 -0
- package/dist/file/sections/identity.d.ts +2 -0
- package/dist/file/sections/identity.js +103 -0
- package/dist/file/sections/index.d.ts +9 -0
- package/dist/file/sections/index.js +43 -0
- package/dist/file/sections/lead.d.ts +4 -0
- package/dist/file/sections/lead.js +18 -0
- package/dist/file/sections/limits.d.ts +2 -0
- package/dist/file/sections/limits.js +43 -0
- package/dist/file/sections/machine.d.ts +2 -0
- package/dist/file/sections/machine.js +38 -0
- package/dist/file/sections/operator.d.ts +2 -0
- package/dist/file/sections/operator.js +11 -0
- package/dist/file/sections/project.d.ts +2 -0
- package/dist/file/sections/project.js +13 -0
- package/dist/file/sections/rules.d.ts +2 -0
- package/dist/file/sections/rules.js +9 -0
- package/dist/file/sections/seats.d.ts +7 -0
- package/dist/file/sections/seats.js +161 -0
- package/dist/file/sections/section.d.ts +32 -0
- package/dist/file/sections/section.js +7 -0
- package/dist/file/sections/session.d.ts +2 -0
- package/dist/file/sections/session.js +16 -0
- package/dist/file/sections/tools.d.ts +2 -0
- package/dist/file/sections/tools.js +43 -0
- package/dist/file/sections/trust.d.ts +2 -0
- package/dist/file/sections/trust.js +20 -0
- package/dist/file/sections/units.d.ts +16 -0
- package/dist/file/sections/units.js +8 -0
- package/dist/file/sections/visibility.d.ts +2 -0
- package/dist/file/sections/visibility.js +12 -0
- package/dist/file/sections/watch-checks.d.ts +9 -0
- package/dist/file/sections/watch-checks.js +14 -0
- package/dist/file/sections/watch.d.ts +14 -0
- package/dist/file/sections/watch.js +111 -0
- package/dist/file/sections/workspace.d.ts +4 -0
- package/dist/file/sections/workspace.js +77 -0
- package/dist/file/types.d.ts +6 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -668
- package/dist/herdr.d.ts +5 -0
- package/dist/herdr.js +51 -12
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +13 -0
- package/dist/launch/deliver.d.ts +7 -1
- package/dist/launch/deliver.js +159 -7
- package/dist/launch/execute.d.ts +4 -2
- package/dist/launch/execute.js +36 -13
- package/dist/launch/plan.js +16 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +25 -2
- package/dist/profiles/claude-code.yaml +51 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +46 -3
- package/dist/profiles/overrides.d.ts +67 -0
- package/dist/profiles/overrides.js +190 -0
- package/dist/profiles/profile.d.ts +3 -0
- package/dist/profiles/profile.js +5 -1
- package/dist/state.d.ts +9 -4
- package/dist/state.js +37 -0
- package/dist/status/compare.js +7 -4
- package/dist/status/statusline.js +3 -1
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +50 -51
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +10 -2
- package/dist/watch/pass.js +70 -29
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +510 -89
- package/dist/watch/screen-data.d.ts +34 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +344 -39
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +25 -6
- package/dist/watch/screen.js +48 -9
- package/examples/team.yaml +8 -0
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { measureSchema, DURATION, PERCENT } from "./units.js";
|
|
2
|
+
import { valueOf } from "./section.js";
|
|
3
|
+
import { defaultWatch } from "./watch.js";
|
|
4
|
+
export const budgets = {
|
|
5
|
+
name: 'budgets',
|
|
6
|
+
owner: true,
|
|
7
|
+
after: ['watch'],
|
|
8
|
+
validate(entry, ctx) {
|
|
9
|
+
const watched = valueOf(ctx, 'watch');
|
|
10
|
+
return readBudgets(entry, ctx.check, watched.watch.quotaMarks, watched.legacyMarks);
|
|
11
|
+
},
|
|
12
|
+
schema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
properties: {
|
|
16
|
+
stale_after: measureSchema(DURATION),
|
|
17
|
+
check_every: measureSchema(DURATION),
|
|
18
|
+
marks: { type: 'array', minItems: 1, items: { type: 'number', exclusiveMinimum: 0, maximum: 100 } },
|
|
19
|
+
accounts: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
kind: { enum: ['subscription', 'spend'] },
|
|
26
|
+
shared: { type: 'boolean' },
|
|
27
|
+
reserve: measureSchema(PERCENT),
|
|
28
|
+
floor: { type: 'string', pattern: '^[0-9]+(?:\\.[0-9]{1,4})? [A-Z]{3}$' },
|
|
29
|
+
sources: { type: 'array', minItems: 1, items: { enum: ['check', 'status_line'] } },
|
|
30
|
+
check: { type: 'string', pattern: '^\\S+$' },
|
|
31
|
+
},
|
|
32
|
+
required: ['kind'],
|
|
33
|
+
allOf: [
|
|
34
|
+
{
|
|
35
|
+
if: { properties: { kind: { const: 'subscription' } }, required: ['kind'] },
|
|
36
|
+
then: { required: ['reserve'] },
|
|
37
|
+
else: { required: ['floor'] },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
$comment: 'a subscription has a reserve, a spend account a floor; a spend account never reads a screen',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The budget values a file that sets none runs with — and, since the section is the owner's, the
|
|
48
|
+
* values a file runs with until the owner approves what it sets instead. No accounts: nothing is
|
|
49
|
+
* budgeted until the owner names one.
|
|
50
|
+
*/
|
|
51
|
+
export function defaultBudgets() {
|
|
52
|
+
return { staleAfter: 30 * 60, checkEvery: 10 * 60, marks: defaultWatch().quotaMarks, accounts: {} };
|
|
53
|
+
}
|
|
54
|
+
function readBudgets(entry, check, fallbackMarks, legacyMarks) {
|
|
55
|
+
const base = { ...defaultBudgets(), marks: fallbackMarks };
|
|
56
|
+
if (!entry)
|
|
57
|
+
return { budgets: base, declared: new Set() };
|
|
58
|
+
const fields = check.fields(entry.value, 'budgets', ['stale_after', 'check_every', 'marks', 'accounts']);
|
|
59
|
+
// Every name the accounts map writes, whether or not the account itself reads cleanly: a seat
|
|
60
|
+
// naming one of these is not also told it named nothing.
|
|
61
|
+
const accounts = fields.get('accounts');
|
|
62
|
+
const declared = new Set(accounts?.value.kind === 'map' ? accounts.value.entries.map((account) => account.key) : []);
|
|
63
|
+
const marks = percentList(fields.get('marks'), 'budgets.marks', check);
|
|
64
|
+
if (marks && legacyMarks) {
|
|
65
|
+
check.warnings.push({ line: fields.get('marks')?.line ?? entry.line, message: 'budgets.marks replaces watch.quota_marks' });
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
budgets: {
|
|
69
|
+
staleAfter: check.measure(fields.get('stale_after'), 'budgets.stale_after', DURATION, '30m') ?? base.staleAfter,
|
|
70
|
+
checkEvery: check.measure(fields.get('check_every'), 'budgets.check_every', DURATION, '10m') ?? base.checkEvery,
|
|
71
|
+
marks: marks ?? fallbackMarks,
|
|
72
|
+
accounts: readAccounts(fields.get('accounts'), check),
|
|
73
|
+
},
|
|
74
|
+
declared,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function percentList(entry, name, check) {
|
|
78
|
+
if (!entry)
|
|
79
|
+
return null;
|
|
80
|
+
const node = entry.value;
|
|
81
|
+
const numbers = node.kind === 'seq' ? node.items.map((item) => (item.kind === 'scalar' ? item.value : null)) : null;
|
|
82
|
+
if (numbers && numbers.length > 0 && numbers.every((n) => typeof n === 'number' && n > 0 && n <= 100))
|
|
83
|
+
return numbers;
|
|
84
|
+
check.fail(node.line, `${name} must be a list of percentages, such as [50, 75, 90]`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function readAccounts(entry, check) {
|
|
88
|
+
const accounts = {};
|
|
89
|
+
if (!entry)
|
|
90
|
+
return accounts;
|
|
91
|
+
if (entry.value.kind !== 'map') {
|
|
92
|
+
check.fail(entry.value.line, 'budgets.accounts must be a map');
|
|
93
|
+
return accounts;
|
|
94
|
+
}
|
|
95
|
+
for (const account of entry.value.entries) {
|
|
96
|
+
const fields = check.fields(account.value, `budgets.accounts.${account.key}`, ['kind', 'shared', 'reserve', 'floor', 'sources', 'check']);
|
|
97
|
+
const kind = check.oneOf(fields.get('kind'), `budgets.accounts.${account.key}.kind`, ['subscription', 'spend']);
|
|
98
|
+
if (!kind) {
|
|
99
|
+
check.fail(account.line, `budgets.accounts.${account.key} needs kind`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const command = check.text(fields.get('check'), `budgets.accounts.${account.key}.check`) ?? null;
|
|
103
|
+
if (command && /\s/.test(command)) {
|
|
104
|
+
check.fail(fields.get('check')?.line ?? account.line, `budgets.accounts.${account.key}.check names one command, with no arguments; use a wrapper`);
|
|
105
|
+
}
|
|
106
|
+
const sources = sourcesOf(fields.get('sources'), account.key, kind, Boolean(command) && !/\s/.test(command ?? ''), check, account.line);
|
|
107
|
+
if (sources.includes('check') && !command)
|
|
108
|
+
check.fail(account.line, `budgets.accounts.${account.key} needs check`);
|
|
109
|
+
if (command && !/\s/.test(command) && !sources.includes('check')) {
|
|
110
|
+
check.fail(fields.get('check')?.line ?? account.line, `budgets.accounts.${account.key}.check needs sources to list check`);
|
|
111
|
+
}
|
|
112
|
+
const measured = kind === 'subscription'
|
|
113
|
+
? check.measure(fields.get('reserve'), `budgets.accounts.${account.key}.reserve`, PERCENT, '20%')
|
|
114
|
+
: undefined;
|
|
115
|
+
const reserve = measured !== undefined && measured > 0 && measured <= 100 ? measured : null;
|
|
116
|
+
if (kind === 'subscription' && measured !== undefined && (measured <= 0 || measured > 100)) {
|
|
117
|
+
check.fail(fields.get('reserve')?.line ?? account.line, `budgets.accounts.${account.key}.reserve must be a percentage above 0 and at most 100, such as 20%`);
|
|
118
|
+
}
|
|
119
|
+
if (kind === 'subscription' && !fields.get('reserve'))
|
|
120
|
+
check.fail(account.line, `budgets.accounts.${account.key} needs reserve`);
|
|
121
|
+
if (kind === 'spend' && fields.get('reserve'))
|
|
122
|
+
check.fail(fields.get('reserve')?.line ?? account.line, 'a spend account has a floor, not a reserve');
|
|
123
|
+
const floor = kind === 'spend' ? money(fields.get('floor'), account.key, check) : null;
|
|
124
|
+
if (kind === 'spend' && !fields.get('floor'))
|
|
125
|
+
check.fail(account.line, `budgets.accounts.${account.key} needs floor`);
|
|
126
|
+
if (kind === 'subscription' && fields.get('floor'))
|
|
127
|
+
check.fail(fields.get('floor')?.line ?? account.line, 'a subscription has a reserve, not a floor');
|
|
128
|
+
accounts[account.key] = {
|
|
129
|
+
kind,
|
|
130
|
+
shared: check.flag(fields.get('shared'), `budgets.accounts.${account.key}.shared`),
|
|
131
|
+
reserve,
|
|
132
|
+
floor,
|
|
133
|
+
sources,
|
|
134
|
+
check: command,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return accounts;
|
|
138
|
+
}
|
|
139
|
+
function sourcesOf(entry, account, kind, hasCheck, check, line) {
|
|
140
|
+
if (!entry) {
|
|
141
|
+
if (hasCheck)
|
|
142
|
+
return ['check'];
|
|
143
|
+
if (kind === 'subscription')
|
|
144
|
+
return ['status_line'];
|
|
145
|
+
check.fail(line, `budgets.accounts.${account} needs check`);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const items = check.list(entry, `budgets.accounts.${account}.sources`);
|
|
149
|
+
if (items.length === 0) {
|
|
150
|
+
check.fail(entry.line, `budgets.accounts.${account} needs a source`);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const sources = [];
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
if (item.value !== 'check' && item.value !== 'status_line') {
|
|
156
|
+
check.fail(item.line, `budgets.accounts.${account}.sources must be check or status_line`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (kind === 'spend' && item.value === 'status_line') {
|
|
160
|
+
check.fail(item.line, 'a spend account never reads a screen');
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (sources.includes(item.value))
|
|
164
|
+
check.fail(item.line, `budgets.accounts.${account}.sources lists ${item.value} twice`);
|
|
165
|
+
else
|
|
166
|
+
sources.push(item.value);
|
|
167
|
+
}
|
|
168
|
+
return sources;
|
|
169
|
+
}
|
|
170
|
+
function money(entry, account, check) {
|
|
171
|
+
if (!entry)
|
|
172
|
+
return null;
|
|
173
|
+
const raw = entry.value.kind === 'scalar' ? entry.value.raw : '';
|
|
174
|
+
const match = /^([0-9]+(?:\.[0-9]{1,4})?) ([A-Z]{3})$/.exec(raw);
|
|
175
|
+
if (!match || match[1] === undefined || match[2] === undefined) {
|
|
176
|
+
check.fail(entry.line, `budgets.accounts.${account}.floor is an amount and a currency, such as 5 USD`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return { amount: Number(match[1]), currency: match[2] };
|
|
180
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { valueOf } from "./section.js";
|
|
2
|
+
import { readLead } from "./lead.js";
|
|
3
|
+
export const coordinator = {
|
|
4
|
+
name: 'coordinator',
|
|
5
|
+
owner: true,
|
|
6
|
+
after: ['seats'],
|
|
7
|
+
validate(entry, ctx) {
|
|
8
|
+
return readLead(entry, 'coordinator', valueOf(ctx, 'seats'), ctx.broken, ctx.check);
|
|
9
|
+
},
|
|
10
|
+
schema: { type: 'string', minLength: 1, $comment: 'must name a declared seat: one, without count, neither parked nor stopped' },
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const format = {
|
|
2
|
+
name: 'format',
|
|
3
|
+
owner: false,
|
|
4
|
+
after: [],
|
|
5
|
+
validate(entry, ctx) {
|
|
6
|
+
if (!entry)
|
|
7
|
+
ctx.check.fail(1, 'format is required: "format: 1"');
|
|
8
|
+
else if (!(entry.value.kind === 'scalar' && entry.value.value === 1 && !entry.value.quoted)) {
|
|
9
|
+
ctx.check.fail(entry.line, 'format must be 1: this version of team reads no other');
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
schema: { const: 1, $comment: 'unquoted: this version of team reads no other' },
|
|
13
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { DEFAULT_COMMIT_TEMPLATE, DEFAULT_FORBIDDEN, DEFAULT_PR_TEMPLATE, isTrailerTemplate, templateProblem, } from "../signature.js";
|
|
2
|
+
const POSITIONS = ['last-line', 'trailer', 'anywhere'];
|
|
3
|
+
export const identity = {
|
|
4
|
+
name: 'identity',
|
|
5
|
+
owner: true,
|
|
6
|
+
after: [],
|
|
7
|
+
validate(entry, ctx) {
|
|
8
|
+
return readIdentity(entry, ctx.check);
|
|
9
|
+
},
|
|
10
|
+
schema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
properties: {
|
|
14
|
+
signature: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
template: { type: 'string' },
|
|
19
|
+
commits: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
template: { type: 'string' },
|
|
24
|
+
position: { enum: POSITIONS },
|
|
25
|
+
exempt: { type: 'array', items: { const: 'merge' } },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
pull_requests: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
template: { type: 'string' },
|
|
33
|
+
position: { enum: POSITIONS },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
humans: { type: 'array', items: { type: 'string', minLength: 1 } },
|
|
39
|
+
since: { type: 'string' },
|
|
40
|
+
forbidden: { type: 'array', items: { type: 'string' } },
|
|
41
|
+
forbidden_public: { type: 'array', items: { type: 'string' } },
|
|
42
|
+
},
|
|
43
|
+
$comment: 'forbidden and forbidden_public are regular expressions; a template in the position "trailer" must read as a git trailer',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
function readIdentity(entry, check) {
|
|
47
|
+
const identity = check.fields(entry?.value, 'identity', ['signature', 'humans', 'since', 'forbidden', 'forbidden_public']);
|
|
48
|
+
const signature = check.fields(identity.get('signature')?.value, 'identity.signature', ['template', 'commits', 'pull_requests']);
|
|
49
|
+
const commits = check.fields(signature.get('commits')?.value, 'identity.signature.commits', ['template', 'position', 'exempt']);
|
|
50
|
+
const prs = check.fields(signature.get('pull_requests')?.value, 'identity.signature.pull_requests', ['template', 'position', 'exempt']);
|
|
51
|
+
const base = readTemplate(signature.get('template'), 'identity.signature.template', check) ?? DEFAULT_COMMIT_TEMPLATE;
|
|
52
|
+
const commitTemplate = readTemplate(commits.get('template'), 'identity.signature.commits.template', check) ?? base;
|
|
53
|
+
const prTemplate = readTemplate(prs.get('template'), 'identity.signature.pull_requests.template', check) ?? DEFAULT_PR_TEMPLATE;
|
|
54
|
+
const commitPosition = check.oneOf(commits.get('position'), 'identity.signature.commits.position', POSITIONS) ?? 'trailer';
|
|
55
|
+
const prPosition = check.oneOf(prs.get('position'), 'identity.signature.pull_requests.position', POSITIONS) ?? 'last-line';
|
|
56
|
+
if (commitPosition === 'trailer' && !isTrailerTemplate(commitTemplate)) {
|
|
57
|
+
const line = commits.get('position')?.line ?? commits.get('template')?.line ?? signature.get('template')?.line ?? entry?.line ?? 1;
|
|
58
|
+
check.fail(line, 'position "trailer" needs a template git reads as a trailer: "<token>: <value>", the token without spaces');
|
|
59
|
+
}
|
|
60
|
+
const exemptEntry = commits.get('exempt');
|
|
61
|
+
const exempt = exemptEntry ? check.list(exemptEntry, 'identity.signature.commits.exempt') : [{ value: 'merge', line: 0 }];
|
|
62
|
+
for (const item of exempt) {
|
|
63
|
+
if (item.value !== 'merge')
|
|
64
|
+
check.fail(item.line, `exempt takes only "merge" in this version, found "${item.value}"`);
|
|
65
|
+
}
|
|
66
|
+
const prExempt = prs.get('exempt');
|
|
67
|
+
if (prExempt)
|
|
68
|
+
check.fail(prExempt.line, 'pull_requests has no exempt: it applies to commits only');
|
|
69
|
+
const forbidden = readPatterns(identity.get('forbidden'), 'identity.forbidden', check);
|
|
70
|
+
const forbiddenPublic = readPatterns(identity.get('forbidden_public'), 'identity.forbidden_public', check);
|
|
71
|
+
return {
|
|
72
|
+
signature: {
|
|
73
|
+
commits: { template: commitTemplate, position: commitPosition, exempt: exempt.some((item) => item.value === 'merge') ? ['merge'] : [] },
|
|
74
|
+
pullRequests: { template: prTemplate, position: prPosition },
|
|
75
|
+
},
|
|
76
|
+
humans: check.list(identity.get('humans'), 'identity.humans').map((item) => item.value),
|
|
77
|
+
since: check.text(identity.get('since'), 'identity.since') ?? null,
|
|
78
|
+
forbidden: [...DEFAULT_FORBIDDEN, ...forbidden.filter((pattern) => !DEFAULT_FORBIDDEN.includes(pattern))],
|
|
79
|
+
forbiddenPublic,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function readTemplate(entry, name, check) {
|
|
83
|
+
const template = check.text(entry, name);
|
|
84
|
+
if (template === undefined || !entry)
|
|
85
|
+
return undefined;
|
|
86
|
+
const problem = templateProblem(template);
|
|
87
|
+
if (problem)
|
|
88
|
+
check.fail(entry.value.line, `${name}: ${problem}`);
|
|
89
|
+
return template;
|
|
90
|
+
}
|
|
91
|
+
function readPatterns(entry, name, check) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const item of check.list(entry, name)) {
|
|
94
|
+
try {
|
|
95
|
+
new RegExp(item.value);
|
|
96
|
+
out.push(item.value);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
check.fail(item.line, `${name}: not a regular expression (${error.message})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Section } from './section.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Every section of the team file, in one order. The owner-only list is generated from this order
|
|
4
|
+
* (`SECTIONS.filter(s => s.owner).map(s => s.name)`), and that order feeds the approval digest,
|
|
5
|
+
* so it is part of the contract: the owner sections appear here exactly as the digest reads them.
|
|
6
|
+
* The validator walks the list running each section once every section it names in `after` has
|
|
7
|
+
* run, which is the order `validate.ts` read them in before the sections were modules.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SECTIONS: readonly Section[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { format } from "./format.js";
|
|
2
|
+
import { project } from "./project.js";
|
|
3
|
+
import { trust } from "./trust.js";
|
|
4
|
+
import { limits } from "./limits.js";
|
|
5
|
+
import { machine } from "./machine.js";
|
|
6
|
+
import { rules } from "./rules.js";
|
|
7
|
+
import { identity } from "./identity.js";
|
|
8
|
+
import { workspace } from "./workspace.js";
|
|
9
|
+
import { coordinator } from "./coordinator.js";
|
|
10
|
+
import { operator } from "./operator.js";
|
|
11
|
+
import { session } from "./session.js";
|
|
12
|
+
import { visibility } from "./visibility.js";
|
|
13
|
+
import { tools } from "./tools.js";
|
|
14
|
+
import { budgets } from "./budgets.js";
|
|
15
|
+
import { watch } from "./watch.js";
|
|
16
|
+
import { watchChecks } from "./watch-checks.js";
|
|
17
|
+
import { seats } from "./seats.js";
|
|
18
|
+
/**
|
|
19
|
+
* Every section of the team file, in one order. The owner-only list is generated from this order
|
|
20
|
+
* (`SECTIONS.filter(s => s.owner).map(s => s.name)`), and that order feeds the approval digest,
|
|
21
|
+
* so it is part of the contract: the owner sections appear here exactly as the digest reads them.
|
|
22
|
+
* The validator walks the list running each section once every section it names in `after` has
|
|
23
|
+
* run, which is the order `validate.ts` read them in before the sections were modules.
|
|
24
|
+
*/
|
|
25
|
+
export const SECTIONS = [
|
|
26
|
+
format,
|
|
27
|
+
project,
|
|
28
|
+
trust,
|
|
29
|
+
limits,
|
|
30
|
+
machine,
|
|
31
|
+
rules,
|
|
32
|
+
identity,
|
|
33
|
+
workspace,
|
|
34
|
+
coordinator,
|
|
35
|
+
operator,
|
|
36
|
+
session,
|
|
37
|
+
visibility,
|
|
38
|
+
tools,
|
|
39
|
+
budgets,
|
|
40
|
+
watch,
|
|
41
|
+
watchChecks,
|
|
42
|
+
seats,
|
|
43
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { YamlEntry } from '../../yaml.ts';
|
|
2
|
+
import type { Check } from '../check.ts';
|
|
3
|
+
import type { DraftSeat } from './seats.ts';
|
|
4
|
+
export declare function readLead(entry: YamlEntry | undefined, field: 'coordinator' | 'operator', seats: DraftSeat[], broken: Set<string>, check: Check): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function readLead(entry, field, seats, broken, check) {
|
|
2
|
+
const name = check.required(entry, field, 1);
|
|
3
|
+
if (!name || !entry)
|
|
4
|
+
return '';
|
|
5
|
+
const seat = seats.find((candidate) => candidate.name === name);
|
|
6
|
+
const line = entry.value.line;
|
|
7
|
+
if (!seat) {
|
|
8
|
+
if (!broken.has(name))
|
|
9
|
+
check.fail(line, `${field} "${name}" names no declared seat`);
|
|
10
|
+
}
|
|
11
|
+
else if (seat.count > 1)
|
|
12
|
+
check.fail(line, `${field} "${name}" can't be a seat with count`);
|
|
13
|
+
else if (seat.parked)
|
|
14
|
+
check.fail(line, `${field} "${name}" can't be a parked seat`);
|
|
15
|
+
else if (seat.stopped)
|
|
16
|
+
check.fail(line, `${field} "${name}" can't be a stopped seat`);
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { valueOf } from "./section.js";
|
|
2
|
+
export const limits = {
|
|
3
|
+
name: 'limits',
|
|
4
|
+
owner: true,
|
|
5
|
+
after: ['seats'],
|
|
6
|
+
validate(entry, ctx) {
|
|
7
|
+
const declared = valueOf(ctx, 'seats').length;
|
|
8
|
+
return readLimits(entry, ctx.check, declared);
|
|
9
|
+
},
|
|
10
|
+
schema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
properties: {
|
|
14
|
+
seats: { type: 'integer', minimum: 1 },
|
|
15
|
+
temporary: { type: 'integer', minimum: 0 },
|
|
16
|
+
vendors: { type: 'object', additionalProperties: { type: 'integer', minimum: 0 } },
|
|
17
|
+
},
|
|
18
|
+
$comment: 'seats defaults to the declared seats plus temporary',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
function readLimits(entry, check, declared) {
|
|
22
|
+
const fields = check.fields(entry?.value, 'limits', ['seats', 'temporary', 'vendors']);
|
|
23
|
+
const temporary = check.whole(fields.get('temporary'), 'limits.temporary', 0) ?? 2;
|
|
24
|
+
const vendors = {};
|
|
25
|
+
const vendorsEntry = fields.get('vendors');
|
|
26
|
+
if (vendorsEntry) {
|
|
27
|
+
const node = vendorsEntry.value;
|
|
28
|
+
if (node.kind !== 'map')
|
|
29
|
+
check.fail(node.line, 'limits.vendors must be a map: a vendor, then its ceiling');
|
|
30
|
+
else {
|
|
31
|
+
for (const vendor of node.entries) {
|
|
32
|
+
const ceiling = check.whole(vendor, `limits.vendors.${vendor.key}`, 0);
|
|
33
|
+
if (ceiling !== undefined)
|
|
34
|
+
vendors[vendor.key] = ceiling;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
seats: check.whole(fields.get('seats'), 'limits.seats', 1) ?? declared + temporary,
|
|
40
|
+
temporary,
|
|
41
|
+
vendors,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { measureSchema, DURATION, PERCENT, SIZE } from "./units.js";
|
|
2
|
+
export const machine = {
|
|
3
|
+
name: 'machine',
|
|
4
|
+
owner: true,
|
|
5
|
+
after: [],
|
|
6
|
+
validate(entry, ctx) {
|
|
7
|
+
return readMachine(entry, ctx.check);
|
|
8
|
+
},
|
|
9
|
+
schema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
properties: {
|
|
13
|
+
load_start: { type: 'number', exclusiveMinimum: 0 },
|
|
14
|
+
load_max: { type: 'number', exclusiveMinimum: 0 },
|
|
15
|
+
memory_start: measureSchema(PERCENT),
|
|
16
|
+
memory_min: measureSchema(PERCENT),
|
|
17
|
+
disk_min: measureSchema(SIZE),
|
|
18
|
+
swap_free_min: measureSchema(SIZE),
|
|
19
|
+
swap_growth_max: measureSchema(SIZE),
|
|
20
|
+
swap_growth_window: measureSchema(DURATION),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function readMachine(entry, check) {
|
|
25
|
+
const fields = check.fields(entry?.value, 'machine', [
|
|
26
|
+
'load_start', 'load_max', 'memory_start', 'memory_min', 'disk_min', 'swap_free_min', 'swap_growth_max', 'swap_growth_window',
|
|
27
|
+
]);
|
|
28
|
+
return {
|
|
29
|
+
loadStart: check.number(fields.get('load_start'), 'machine.load_start') ?? 3,
|
|
30
|
+
loadMax: check.number(fields.get('load_max'), 'machine.load_max') ?? 6,
|
|
31
|
+
memoryStart: check.measure(fields.get('memory_start'), 'machine.memory_start', PERCENT, '25%') ?? 25,
|
|
32
|
+
memoryMin: check.measure(fields.get('memory_min'), 'machine.memory_min', PERCENT, '15%') ?? 15,
|
|
33
|
+
diskMin: check.measure(fields.get('disk_min'), 'machine.disk_min', SIZE, '10GB') ?? 10e9,
|
|
34
|
+
swapFreeMin: check.measure(fields.get('swap_free_min'), 'machine.swap_free_min', SIZE, '2GB') ?? 2e9,
|
|
35
|
+
swapGrowthMax: check.measure(fields.get('swap_growth_max'), 'machine.swap_growth_max', SIZE, '1GB') ?? 1e9,
|
|
36
|
+
swapGrowthWindow: check.measure(fields.get('swap_growth_window'), 'machine.swap_growth_window', DURATION, '10m') ?? 600,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { valueOf } from "./section.js";
|
|
2
|
+
import { readLead } from "./lead.js";
|
|
3
|
+
export const operator = {
|
|
4
|
+
name: 'operator',
|
|
5
|
+
owner: true,
|
|
6
|
+
after: ['seats'],
|
|
7
|
+
validate(entry, ctx) {
|
|
8
|
+
return readLead(entry, 'operator', valueOf(ctx, 'seats'), ctx.broken, ctx.check);
|
|
9
|
+
},
|
|
10
|
+
schema: { type: 'string', minLength: 1, $comment: 'must name a declared seat: one, without count, neither parked nor stopped' },
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const project = {
|
|
2
|
+
name: 'project',
|
|
3
|
+
owner: false,
|
|
4
|
+
after: [],
|
|
5
|
+
validate(entry, ctx) {
|
|
6
|
+
const project = ctx.check.required(entry, 'project', 1) ?? '';
|
|
7
|
+
if (project && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(project)) {
|
|
8
|
+
ctx.check.fail(entry?.line ?? 1, 'project must be a short name: letters, digits, ".", "_" and "-"');
|
|
9
|
+
}
|
|
10
|
+
return project;
|
|
11
|
+
},
|
|
12
|
+
schema: { type: 'string', minLength: 1, pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*$' },
|
|
13
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Mode, Seat } from '../types.ts';
|
|
2
|
+
import type { Section } from './section.ts';
|
|
3
|
+
export type DraftSeat = Omit<Seat, 'mode'> & {
|
|
4
|
+
mode?: Mode;
|
|
5
|
+
};
|
|
6
|
+
export declare const seats: Section;
|
|
7
|
+
export declare function defaultLabel(model: string, version: string): string;
|