@warren-dean/code-kit 0.19.0 → 0.21.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +34 -29
- package/bin/code-kit.mjs +240 -8
- package/hooks/guard-bash.mjs +45 -3
- package/hooks/guard-paths.mjs +3 -2
- package/hooks/hooks.json +22 -0
- package/hooks/lib/adapters/hub/client.mjs +723 -0
- package/hooks/lib/adapters/hub/index.mjs +25 -0
- package/hooks/lib/adapters/index.mjs +35 -0
- package/hooks/lib/checks.mjs +5 -1
- package/hooks/lib/config.mjs +1 -1
- package/hooks/lib/instructions.mjs +46 -13
- package/hooks/lib/layers.mjs +11 -0
- package/hooks/lib/plan.mjs +24 -0
- package/hooks/lib/rules.mjs +2 -1
- package/hooks/lib/snapshot.mjs +424 -0
- package/hooks/lib/standards.mjs +450 -0
- package/hooks/lib/team.mjs +55 -9
- package/hooks/lib/teamfacts.mjs +10 -6
- package/hooks/lib/verify.mjs +23 -1
- package/hooks/mod/register.mjs +35 -0
- package/hooks/mod/register.test.ts +49 -4
- package/hooks/mod/review.mjs +1 -0
- package/hooks/mod/teamview.mjs +8 -2
- package/hooks/mod/view.mjs +36 -0
- package/hooks/post-edit-check.mjs +5 -1
- package/hooks/stop-check.mjs +11 -3
- package/hooks/team-session.mjs +57 -0
- package/package.json +1 -1
- package/skills/init/SKILL.md +16 -2
- package/skills/next/SKILL.md +1 -1
- package/skills/review/SKILL.md +2 -1
- package/skills/spec-design/SKILL.md +8 -6
- package/skills/team-init/SKILL.md +3 -3
- package/templates/agent.md +1 -0
- package/templates/claude-md.md +2 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// HUBR-2: what a project reports to its team provider (docs/specs/21-hub-adapter-and-reports.md): a
|
|
2
|
+
// snapshot of facts code-kit already has (status, timeline, trail, journal's approvals, the merge queue,
|
|
3
|
+
// usage, the rules), as metadata only. SNAPSHOT_SCHEMA is the allow-list: every field a snapshot may
|
|
4
|
+
// hold, and what kind of value it is. A provider validates against the same list, and `validSnapshot`
|
|
5
|
+
// refuses anything else, so file contents, diffs, prompts and messages have nowhere to go.
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { APPROVAL_LOG } from './rules.mjs';
|
|
11
|
+
import { approverOf } from './journal.mjs';
|
|
12
|
+
import { readQueue } from './queue.mjs';
|
|
13
|
+
import { sendBacks } from './reviews.mjs';
|
|
14
|
+
import { readMilestones, specCheckRows } from './plan.mjs';
|
|
15
|
+
import { storyTimes } from './timeline.mjs';
|
|
16
|
+
import { trail } from './storymap.mjs';
|
|
17
|
+
import {
|
|
18
|
+
APPLIED,
|
|
19
|
+
CATEGORIES,
|
|
20
|
+
LEVELS,
|
|
21
|
+
ORIGINS,
|
|
22
|
+
SOURCES,
|
|
23
|
+
projectStandards,
|
|
24
|
+
sourceOf,
|
|
25
|
+
} from './standards.mjs';
|
|
26
|
+
|
|
27
|
+
/** 2 adds the project's standards (code-kit spec 22). */
|
|
28
|
+
export const SNAPSHOT_VERSION = 2;
|
|
29
|
+
|
|
30
|
+
/** A story's stage, as a report names it. */
|
|
31
|
+
export const STAGES = [
|
|
32
|
+
'planned',
|
|
33
|
+
'claimed',
|
|
34
|
+
'building',
|
|
35
|
+
'review',
|
|
36
|
+
'sent-back',
|
|
37
|
+
'queued',
|
|
38
|
+
'merged',
|
|
39
|
+
'done',
|
|
40
|
+
];
|
|
41
|
+
export const AGENT_KINDS = ['lead', 'lane', 'reviewer', 'security-reviewer', 'background'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The allow-list. A leaf is a kind: `id` (ST-4, API-2), `handle` (a GitHub login), `name` (a lane, a
|
|
45
|
+
* layer, a milestone, an approval: one line, at most 120 characters), `title` (one line, at most 200),
|
|
46
|
+
* `line` (a send-back's reason: one line, at most 200), `path` (a file path or glob: one line, at most
|
|
47
|
+
* 300), `ms` (a time in milliseconds), `date` (YYYY-MM-DD), `count` (a whole number, 0 or more),
|
|
48
|
+
* `bool`, `enum:a|b`. A trailing `?` allows null. An array holds one shape; an object holds only the
|
|
49
|
+
* keys it lists.
|
|
50
|
+
*/
|
|
51
|
+
export const SNAPSHOT_SCHEMA = {
|
|
52
|
+
version: 'count',
|
|
53
|
+
project: { repository: 'name?', base: 'name', codeKit: 'name', at: 'ms' },
|
|
54
|
+
milestones: [{ name: 'name', target: 'date?', stories: ['id'] }],
|
|
55
|
+
stories: [
|
|
56
|
+
{
|
|
57
|
+
id: 'id',
|
|
58
|
+
title: 'title',
|
|
59
|
+
lane: 'name?',
|
|
60
|
+
milestone: 'name?',
|
|
61
|
+
requirements: ['id'],
|
|
62
|
+
dependsOn: ['id'],
|
|
63
|
+
stage: `enum:${STAGES.join('|')}`,
|
|
64
|
+
since: {
|
|
65
|
+
building: 'ms?',
|
|
66
|
+
review: 'ms?',
|
|
67
|
+
sentBack: 'ms?',
|
|
68
|
+
queued: 'ms?',
|
|
69
|
+
merged: 'ms?',
|
|
70
|
+
},
|
|
71
|
+
claimedBy: 'handle?',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
sendBacks: [{ story: 'id', at: 'ms', reason: 'line?' }],
|
|
75
|
+
requirements: [
|
|
76
|
+
{
|
|
77
|
+
id: 'id',
|
|
78
|
+
stories: ['id'],
|
|
79
|
+
built: ['path'],
|
|
80
|
+
tested: ['path'],
|
|
81
|
+
check: 'enum:done|partial|missing|conflicts|none',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
approvals: [{ what: 'name', path: 'path?', who: 'enum:person|lead', at: 'ms', branch: 'name?' }],
|
|
85
|
+
usage: [
|
|
86
|
+
{
|
|
87
|
+
story: 'id?',
|
|
88
|
+
kind: `enum:${AGENT_KINDS.join('|')}`,
|
|
89
|
+
input: 'count',
|
|
90
|
+
output: 'count',
|
|
91
|
+
cache: 'count',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
rules: {
|
|
95
|
+
layers: [{ name: 'name', mayImport: ['name'] }],
|
|
96
|
+
protected: [{ glob: 'path', approval: 'name' }],
|
|
97
|
+
delegated: { protected: ['name'], dependencies: 'bool', merge: 'bool' },
|
|
98
|
+
designGate: 'bool',
|
|
99
|
+
},
|
|
100
|
+
// Spec 22: each standard that applies, by id and title, how it's applied and where it came from;
|
|
101
|
+
// its why in one line at most, and its evidence only as counts.
|
|
102
|
+
standards: [
|
|
103
|
+
{
|
|
104
|
+
id: 'id',
|
|
105
|
+
title: 'title',
|
|
106
|
+
category: `enum:${Object.values(CATEGORIES).join('|')}`,
|
|
107
|
+
level: `enum:${LEVELS.join('|')}`,
|
|
108
|
+
applied: `enum:${APPLIED.join('|')}`,
|
|
109
|
+
layers: ['name'],
|
|
110
|
+
source: `enum:${SOURCES.join('|')}`,
|
|
111
|
+
evidenceCount: 'count?',
|
|
112
|
+
evidenceOf: 'count?',
|
|
113
|
+
origin: `enum:${ORIGINS.join('|')}`,
|
|
114
|
+
why: 'line?',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Parts a report from an older code-kit doesn't have yet: valid when missing, checked when there. */
|
|
120
|
+
export const LATER_PARTS = ['standards'];
|
|
121
|
+
|
|
122
|
+
const ONE_LINE = (max) => (v) => typeof v === 'string' && v.length <= max && !/[\r\n]/.test(v);
|
|
123
|
+
const KINDS = {
|
|
124
|
+
id: (v) => typeof v === 'string' && /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(v),
|
|
125
|
+
handle: (v) => typeof v === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(v),
|
|
126
|
+
name: ONE_LINE(120),
|
|
127
|
+
title: ONE_LINE(200),
|
|
128
|
+
line: ONE_LINE(200),
|
|
129
|
+
path: ONE_LINE(300),
|
|
130
|
+
ms: (v) => Number.isInteger(v) && v > 0,
|
|
131
|
+
date: (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v),
|
|
132
|
+
count: (v) => Number.isInteger(v) && v >= 0,
|
|
133
|
+
bool: (v) => typeof v === 'boolean',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** What's wrong with a snapshot against the allow-list, as `path: problem` lines; empty when valid. */
|
|
137
|
+
export function snapshotProblems(value, shape = SNAPSHOT_SCHEMA, at = 'snapshot') {
|
|
138
|
+
if (typeof shape === 'string') {
|
|
139
|
+
const optional = shape.endsWith('?');
|
|
140
|
+
const kind = optional ? shape.slice(0, -1) : shape;
|
|
141
|
+
if (value === null || value === undefined) return optional ? [] : [`${at}: missing`];
|
|
142
|
+
if (kind.startsWith('enum:'))
|
|
143
|
+
return kind.slice(5).split('|').includes(value) ? [] : [`${at}: not one of ${kind.slice(5)}`];
|
|
144
|
+
return KINDS[kind](value) ? [] : [`${at}: not a ${kind}`];
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(shape)) {
|
|
147
|
+
if (!Array.isArray(value)) return [`${at}: not a list`];
|
|
148
|
+
return value.flatMap((v, i) => snapshotProblems(v, shape[0], `${at}[${i}]`));
|
|
149
|
+
}
|
|
150
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
151
|
+
return [`${at}: not an object`];
|
|
152
|
+
const extra = Object.keys(value).filter((k) => !(k in shape));
|
|
153
|
+
return [
|
|
154
|
+
...extra.map((k) => `${at}.${k}: not a field a report may hold`),
|
|
155
|
+
...Object.entries(shape)
|
|
156
|
+
.filter(([k]) => !(shape === SNAPSHOT_SCHEMA && LATER_PARTS.includes(k) && !(k in value)))
|
|
157
|
+
.flatMap(([k, s]) => snapshotProblems(value[k], s, `${at}.${k}`)),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The snapshot's hash, leaving out when it was made: equal snapshots aren't sent twice. */
|
|
162
|
+
export function snapshotHash(snapshot, parts = null) {
|
|
163
|
+
const { project, ...rest } = snapshot;
|
|
164
|
+
const kept = parts ? Object.fromEntries(parts.map((p) => [p, rest[p]])) : rest;
|
|
165
|
+
return createHash('sha256')
|
|
166
|
+
.update(JSON.stringify({ project: { ...project, at: 0 }, ...kept }))
|
|
167
|
+
.digest('hex');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The parts whose change is sent at once (HUBR-3); usage alone waits for the ten minutes. */
|
|
171
|
+
export const EVENT_PARTS = [
|
|
172
|
+
'milestones',
|
|
173
|
+
'stories',
|
|
174
|
+
'sendBacks',
|
|
175
|
+
'requirements',
|
|
176
|
+
'approvals',
|
|
177
|
+
'rules',
|
|
178
|
+
'standards',
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const git = (root, ...args) => {
|
|
182
|
+
try {
|
|
183
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
184
|
+
encoding: 'utf8',
|
|
185
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
186
|
+
}).trim();
|
|
187
|
+
} catch {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** owner/name of the repository's GitHub origin, or null. */
|
|
193
|
+
export function repositoryOf(root) {
|
|
194
|
+
const remote = git(root, 'config', '--get', 'remote.origin.url');
|
|
195
|
+
const m = remote.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
196
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The main checkout, where the lanes' worktrees share their state. */
|
|
200
|
+
export function mainCheckout(root) {
|
|
201
|
+
const common = git(root, 'rev-parse', '--path-format=absolute', '--git-common-dir');
|
|
202
|
+
return common ? dirname(common) : root;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const USAGE_DIR = '.claude/state/usage';
|
|
206
|
+
|
|
207
|
+
/** The usage every session recorded (USE-1's ledgers, written by the mod): [{ story, kind, … }]. */
|
|
208
|
+
export function recordedUsage(root) {
|
|
209
|
+
const dir = join(mainCheckout(root), USAGE_DIR);
|
|
210
|
+
const sums = new Map();
|
|
211
|
+
let files = [];
|
|
212
|
+
try {
|
|
213
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
for (const f of files) {
|
|
218
|
+
let entries = [];
|
|
219
|
+
try {
|
|
220
|
+
entries = Object.values(JSON.parse(readFileSync(join(dir, f), 'utf8')).entries ?? {});
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const e of entries) {
|
|
225
|
+
if (!AGENT_KINDS.includes(e.kind)) continue;
|
|
226
|
+
const story = KINDS.id(e.story) ? e.story : null;
|
|
227
|
+
const key = `${story}|${e.kind}`;
|
|
228
|
+
const was = sums.get(key) ?? { story, kind: e.kind, input: 0, output: 0, cache: 0 };
|
|
229
|
+
for (const k of ['input', 'output', 'cache'])
|
|
230
|
+
was[k] += Number.isInteger(e[k]) && e[k] > 0 ? e[k] : 0;
|
|
231
|
+
sums.set(key, was);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return [...sums.values()];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The spec-check report's status for each requirement of a story, from its branch's report. */
|
|
238
|
+
function checkedRows(root, story) {
|
|
239
|
+
const name = `${story.branch.replace(/[^\w.-]+/g, '_')}.md`;
|
|
240
|
+
for (const dir of [story.worktree, root].filter(Boolean)) {
|
|
241
|
+
const file = join(dir, '.claude/state/spec-check', name);
|
|
242
|
+
if (existsSync(file)) return specCheckRows(readFileSync(file, 'utf8'));
|
|
243
|
+
}
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const CHECKS = ['done', 'partial', 'missing', 'conflicts'];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The snapshot (HUBR-2) from what code-kit already computed: `status` is buildStatus's, `base` the
|
|
251
|
+
* branch stories merge into, `claims` the team's claims as last read, `version` code-kit's.
|
|
252
|
+
*/
|
|
253
|
+
export function buildSnapshot({
|
|
254
|
+
root,
|
|
255
|
+
config,
|
|
256
|
+
status,
|
|
257
|
+
base,
|
|
258
|
+
claims = [],
|
|
259
|
+
version,
|
|
260
|
+
now = Date.now(),
|
|
261
|
+
}) {
|
|
262
|
+
const times = new Map(storyTimes(root, status.stories, base).map((t) => [t.id, t]));
|
|
263
|
+
const queued = new Map(
|
|
264
|
+
readQueue(root)
|
|
265
|
+
.filter((e) => e.state === 'waiting' || e.state === 'merging')
|
|
266
|
+
.map((e) => [e.branch, Date.parse(e.passed)]),
|
|
267
|
+
);
|
|
268
|
+
const backs = sendBacks(root);
|
|
269
|
+
const claimed = new Map(claims.map((c) => [c.story, c.by]));
|
|
270
|
+
const ms = (v) => (Number.isInteger(v) && v > 0 ? v : null);
|
|
271
|
+
const line = (s) => (s ? String(s).replace(/\s+/g, ' ').trim().slice(0, 200) || null : null);
|
|
272
|
+
|
|
273
|
+
const stories = status.stories.map((s) => {
|
|
274
|
+
const t = times.get(s.id) ?? {};
|
|
275
|
+
const myBacks = backs.filter((b) => b.branch === s.branch);
|
|
276
|
+
const lastBack = myBacks.length ? Date.parse(myBacks.at(-1).at) : null;
|
|
277
|
+
let stage = 'planned';
|
|
278
|
+
if (s.state === 'done') stage = s.status === 'done' ? 'done' : 'merged';
|
|
279
|
+
else if (s.state === 'sent back') stage = 'sent-back';
|
|
280
|
+
else if (s.state === 'review') stage = queued.has(s.branch) ? 'queued' : 'review';
|
|
281
|
+
else if (s.state === 'in progress') stage = 'building';
|
|
282
|
+
else if (claimed.has(s.id)) stage = 'claimed';
|
|
283
|
+
const reached = STAGES.indexOf(stage);
|
|
284
|
+
return {
|
|
285
|
+
id: s.id,
|
|
286
|
+
title: line(s.title) ?? s.id,
|
|
287
|
+
lane: s.lane || null,
|
|
288
|
+
milestone: s.milestone ? line(s.milestone).slice(0, 120) : null,
|
|
289
|
+
requirements: s.requirements,
|
|
290
|
+
dependsOn: s.dependsOn,
|
|
291
|
+
stage,
|
|
292
|
+
since: {
|
|
293
|
+
building: reached >= STAGES.indexOf('building') ? ms(t.started) : null,
|
|
294
|
+
review:
|
|
295
|
+
reached >= STAGES.indexOf('review') && stage !== 'sent-back' ? ms(t.lastCommit) : null,
|
|
296
|
+
sentBack: ms(lastBack),
|
|
297
|
+
queued: stage === 'queued' ? ms(queued.get(s.branch)) : null,
|
|
298
|
+
merged: ms(t.merged),
|
|
299
|
+
},
|
|
300
|
+
claimedBy: claimed.get(s.id) ?? null,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const targets = Object.fromEntries(
|
|
305
|
+
readMilestones(root, config.docs.plan).map((m) => [m.name, m.target]),
|
|
306
|
+
);
|
|
307
|
+
const milestones = [...new Set(status.stories.map((s) => s.milestone).filter(Boolean))].map(
|
|
308
|
+
(m) => ({
|
|
309
|
+
name: line(m).slice(0, 120),
|
|
310
|
+
target: targets[m] ?? null,
|
|
311
|
+
stories: status.stories.filter((s) => s.milestone === m).map((s) => s.id),
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const byBranch = new Map(status.stories.map((s) => [s.branch, s.id]));
|
|
316
|
+
const sendBackList = backs
|
|
317
|
+
.filter((b) => byBranch.has(b.branch) && ms(Date.parse(b.at)))
|
|
318
|
+
.map((b) => ({ story: byBranch.get(b.branch), at: Date.parse(b.at), reason: line(b.reason) }));
|
|
319
|
+
|
|
320
|
+
const checks = new Map();
|
|
321
|
+
for (const s of status.stories)
|
|
322
|
+
if (s.branchExists)
|
|
323
|
+
for (const row of checkedRows(root, s))
|
|
324
|
+
if (CHECKS.includes(row.status)) checks.set(row.requirement, row.status);
|
|
325
|
+
const requirements = trail(root, status, base).map((r) => ({
|
|
326
|
+
id: r.id,
|
|
327
|
+
stories: r.stories.map((s) => s.id),
|
|
328
|
+
built: r.files.filter((f) => KINDS.path(f)),
|
|
329
|
+
tested: r.tests.filter((f) => KINDS.path(f)),
|
|
330
|
+
check: checks.get(r.id) ?? 'none',
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
const approvals = [];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
for (const raw of git(root, 'show', `${base}:${APPROVAL_LOG}`).split('\n').filter(Boolean)) {
|
|
336
|
+
let e;
|
|
337
|
+
try {
|
|
338
|
+
e = JSON.parse(raw);
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!e.approval || e.approval === 'lead') continue;
|
|
343
|
+
const at = Date.parse(e.grantedAt ?? e.at);
|
|
344
|
+
const key = `${e.approval}|${e.file ?? ''}|${at}`;
|
|
345
|
+
if (seen.has(key) || !ms(at) || !KINDS.name(e.approval)) continue;
|
|
346
|
+
seen.add(key);
|
|
347
|
+
approvals.push({
|
|
348
|
+
what: e.approval,
|
|
349
|
+
path: KINDS.path(e.file) ? e.file : null,
|
|
350
|
+
who: approverOf(e),
|
|
351
|
+
at,
|
|
352
|
+
branch: KINDS.name(e.branch) ? e.branch : null,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const delegate = config.approvals?.delegate ?? null;
|
|
357
|
+
const snapshot = {
|
|
358
|
+
version: SNAPSHOT_VERSION,
|
|
359
|
+
project: {
|
|
360
|
+
repository: repositoryOf(root),
|
|
361
|
+
base,
|
|
362
|
+
codeKit: String(version),
|
|
363
|
+
at: now,
|
|
364
|
+
},
|
|
365
|
+
milestones,
|
|
366
|
+
stories,
|
|
367
|
+
sendBacks: sendBackList,
|
|
368
|
+
requirements,
|
|
369
|
+
approvals,
|
|
370
|
+
usage: recordedUsage(root),
|
|
371
|
+
rules: {
|
|
372
|
+
layers: (config.layers ?? []).map((l) => ({ name: l.name, mayImport: l.mayImport ?? [] })),
|
|
373
|
+
protected: (config.protected ?? [])
|
|
374
|
+
.filter((p) => KINDS.path(p.glob) && KINDS.name(p.approval))
|
|
375
|
+
.map((p) => ({ glob: p.glob, approval: p.approval })),
|
|
376
|
+
delegated: {
|
|
377
|
+
protected: delegate?.protected ?? [],
|
|
378
|
+
dependencies: Boolean(delegate?.dependencies),
|
|
379
|
+
merge: Boolean(delegate?.merge),
|
|
380
|
+
},
|
|
381
|
+
designGate: Boolean(config.designGate),
|
|
382
|
+
},
|
|
383
|
+
standards: reportedStandards(root, config, line),
|
|
384
|
+
};
|
|
385
|
+
return snapshot;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Spec 22: the standards that apply, as a report holds them. One that the file can't make sense of (an
|
|
390
|
+
* unknown category, no level) is left out: `code-kit standards` tells the project what's wrong with it.
|
|
391
|
+
*/
|
|
392
|
+
function reportedStandards(root, config, line) {
|
|
393
|
+
let all = [];
|
|
394
|
+
try {
|
|
395
|
+
all = projectStandards(root, config).standards;
|
|
396
|
+
} catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return all
|
|
400
|
+
.filter(
|
|
401
|
+
(s) =>
|
|
402
|
+
KINDS.id(s.id) &&
|
|
403
|
+
CATEGORIES[s.id.split('-')[0]] &&
|
|
404
|
+
LEVELS.includes(s.level) &&
|
|
405
|
+
APPLIED.includes(s.applied) &&
|
|
406
|
+
KINDS.title(line(s.title) ?? ''),
|
|
407
|
+
)
|
|
408
|
+
.map((s) => {
|
|
409
|
+
const source = s.origin === 'inherited' ? { kind: 'organisation' } : sourceOf(s.source);
|
|
410
|
+
return {
|
|
411
|
+
id: s.id,
|
|
412
|
+
title: line(s.title),
|
|
413
|
+
category: CATEGORIES[s.id.split('-')[0]],
|
|
414
|
+
level: s.level,
|
|
415
|
+
applied: s.applied,
|
|
416
|
+
layers: (s.layers ?? []).filter((l) => KINDS.name(l)),
|
|
417
|
+
source: source.kind,
|
|
418
|
+
evidenceCount: source.count ?? null,
|
|
419
|
+
evidenceOf: source.of ?? null,
|
|
420
|
+
origin: ORIGINS.includes(s.origin) ? s.origin : 'own',
|
|
421
|
+
why: line(s.why),
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|