@warren-dean/code-kit 0.20.0 → 0.22.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 +24 -0
- package/README.md +75 -50
- package/bin/code-kit.mjs +364 -25
- package/hooks/guard-agent.mjs +36 -0
- package/hooks/guard-bash.mjs +67 -13
- package/hooks/guard-paths.mjs +14 -2
- package/hooks/hooks.json +20 -0
- package/hooks/lane-audit.mjs +38 -1
- package/hooks/lib/adapters/hub/client.mjs +15 -0
- package/hooks/lib/checks.mjs +5 -1
- package/hooks/lib/config.mjs +149 -6
- package/hooks/lib/decisionstore.mjs +89 -0
- package/hooks/lib/harness.mjs +7 -0
- package/hooks/lib/hook.mjs +7 -0
- package/hooks/lib/instructions.mjs +46 -13
- package/hooks/lib/laneclaims.mjs +89 -0
- package/hooks/lib/layers.mjs +11 -0
- package/hooks/lib/loop-state.mjs +20 -2
- package/hooks/lib/next.mjs +34 -1
- package/hooks/lib/plan.mjs +3 -1
- package/hooks/lib/questions.mjs +23 -5
- package/hooks/lib/regression.mjs +2 -1
- package/hooks/lib/rules.mjs +20 -13
- package/hooks/lib/schedule.mjs +86 -0
- package/hooks/lib/sessions.mjs +223 -0
- package/hooks/lib/snapshot.mjs +80 -2
- package/hooks/lib/standards.mjs +450 -0
- package/hooks/lib/team.mjs +9 -2
- package/hooks/lib/verify.mjs +23 -1
- package/hooks/mod/decisions.mjs +1 -28
- package/hooks/mod/loop.mjs +8 -0
- package/hooks/mod/mission.mjs +21 -19
- package/hooks/mod/register.mjs +117 -40
- package/hooks/mod/register.test.ts +248 -73
- package/hooks/mod/review.mjs +1 -0
- package/hooks/mod/view.mjs +98 -47
- package/hooks/post-edit-check.mjs +8 -1
- package/hooks/session.mjs +24 -0
- package/hooks/stop-check.mjs +23 -3
- package/package.json +1 -1
- package/skills/decide/SKILL.md +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/init/SKILL.md +17 -3
- package/skills/lead/SKILL.md +28 -0
- package/skills/next/SKILL.md +2 -2
- package/skills/review/SKILL.md +2 -1
- package/skills/spec-design/SKILL.md +10 -7
- package/templates/agent.md +1 -0
- package/templates/claude-md.md +5 -1
- package/templates/plan.md +2 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Sessions as members of the team (docs/specs/23-the-team.md, SESS-1 to SESS-5). Several Claude Code
|
|
2
|
+
// sessions can work one project at once; each is a member, the person and the session, as spec 16 makes
|
|
3
|
+
// each developer one. One is the lead session (the loop runs there, dispatches start from it); the rest
|
|
4
|
+
// are working sessions, which stay quiet and record an ad hoc task, with the paths it leases, before
|
|
5
|
+
// they change code. A session's id is Claude Code's: `session_id` in hooks, `$.session.id()` in the mod,
|
|
6
|
+
// `CLAUDE_CODE_SESSION_ID` in every command an agent runs. Kept in the main checkout, one file per session,
|
|
7
|
+
// so every session and worktree on the machine reads the same: this is the local provider of SESS-1.
|
|
8
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { userInfo } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { matchesAny } from './glob.mjs';
|
|
12
|
+
import { CLI, projectWorktrees } from './rules.mjs';
|
|
13
|
+
import { shortSession } from './laneclaims.mjs';
|
|
14
|
+
|
|
15
|
+
export { shortSession };
|
|
16
|
+
export const SESSIONS_DIR = '.claude/state/sessions';
|
|
17
|
+
/** A session seen within this is live; one not seen for longer has ended, whether or not it said so. */
|
|
18
|
+
export const SESSION_LIVE_MS = 15 * 60_000;
|
|
19
|
+
/** Hooks note that a session is still there at most this often. */
|
|
20
|
+
const SEEN_EVERY_MS = 60_000;
|
|
21
|
+
/** The uncommitted files a session wrote, kept for SESS-5: at most this many. */
|
|
22
|
+
const WROTE_KEPT = 500;
|
|
23
|
+
|
|
24
|
+
const main = (root) => projectWorktrees(root)[0] ?? root;
|
|
25
|
+
const dirOf = (root) => join(main(root), SESSIONS_DIR);
|
|
26
|
+
const safe = (id) => String(id).replace(/[^\w.-]/g, '_');
|
|
27
|
+
const fileOf = (root, id) => join(dirOf(root), `${safe(id)}.json`);
|
|
28
|
+
|
|
29
|
+
/** The person at this machine: their GitHub login if code-kit has it cached, or the OS user. */
|
|
30
|
+
export function personHere(root) {
|
|
31
|
+
try {
|
|
32
|
+
const cached = JSON.parse(readFileSync(join(main(root), '.claude/state/whoami.json'), 'utf8'));
|
|
33
|
+
if (cached?.login) return cached.login;
|
|
34
|
+
} catch {
|
|
35
|
+
// not cached: the OS user
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return userInfo().username || null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** SESS-1: who a claim, approval, question or decision belongs to: `<person>/<short id>`. */
|
|
45
|
+
export const memberOf = (record) =>
|
|
46
|
+
record ? `${record.person ?? 'someone'}/${shortSession(record.id)}` : null;
|
|
47
|
+
|
|
48
|
+
export function readSession(root, id) {
|
|
49
|
+
if (!id) return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(fileOf(root, id), 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function write(root, record) {
|
|
58
|
+
mkdirSync(dirOf(root), { recursive: true });
|
|
59
|
+
writeFileSync(fileOf(root, record.id), `${JSON.stringify(record)}\n`);
|
|
60
|
+
return record;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isLive = (r, now) => !r.ended && now - Date.parse(r.seen ?? r.started) < SESSION_LIVE_MS;
|
|
64
|
+
|
|
65
|
+
/** Every session the project has seen: [{ ...record, live }], the newest first. */
|
|
66
|
+
export function sessions(root, now = Date.now()) {
|
|
67
|
+
let files = [];
|
|
68
|
+
try {
|
|
69
|
+
files = readdirSync(dirOf(root)).filter((f) => f.endsWith('.json'));
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return files
|
|
74
|
+
.flatMap((f) => {
|
|
75
|
+
try {
|
|
76
|
+
const r = JSON.parse(readFileSync(join(dirOf(root), f), 'utf8'));
|
|
77
|
+
return r?.id ? [{ ...r, live: isLive(r, now) }] : [];
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.sort((a, b) => Date.parse(b.started) - Date.parse(a.started));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** SESS-2: the live lead session, or null. */
|
|
86
|
+
export const leadSession = (root, now = Date.now()) =>
|
|
87
|
+
sessions(root, now).find((s) => s.live && s.role === 'lead') ?? null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SESS-2: a session starting (or resumed). It's the lead when no other live session is; otherwise a
|
|
91
|
+
* working session. A resumed session keeps the role it had, unless it was the lead and another session
|
|
92
|
+
* has led since.
|
|
93
|
+
*/
|
|
94
|
+
export function startSession(root, id, now = Date.now()) {
|
|
95
|
+
const at = new Date(now).toISOString();
|
|
96
|
+
const had = readSession(root, id);
|
|
97
|
+
const lead = leadSession(root, now);
|
|
98
|
+
return write(root, {
|
|
99
|
+
id,
|
|
100
|
+
person: had?.person ?? personHere(root),
|
|
101
|
+
role: lead && lead.id !== id ? 'working' : 'lead',
|
|
102
|
+
started: had?.started ?? at,
|
|
103
|
+
seen: at,
|
|
104
|
+
ended: null,
|
|
105
|
+
task: had?.task ?? null,
|
|
106
|
+
wrote: had?.wrote ?? [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A hook saw the session act: it's still there. A session code-kit hadn't seen starts here. */
|
|
111
|
+
export function seeSession(root, id, now = Date.now()) {
|
|
112
|
+
if (!id) return null;
|
|
113
|
+
const had = readSession(root, id);
|
|
114
|
+
if (!had || had.ended) return startSession(root, id, now);
|
|
115
|
+
// A lead that went quiet past SESSION_LIVE_MS, while another session took the lead, works now.
|
|
116
|
+
const lead = had.role === 'lead' ? leadSession(root, now) : null;
|
|
117
|
+
const demoted = lead && lead.id !== id;
|
|
118
|
+
if (!demoted && now - Date.parse(had.seen) < SEEN_EVERY_MS) return had;
|
|
119
|
+
return write(root, {
|
|
120
|
+
...had,
|
|
121
|
+
seen: new Date(now).toISOString(),
|
|
122
|
+
...(demoted ? { role: 'working' } : {}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The session ended: it's no longer live, and leads nothing. Its task stays, stale, for SESS-4. */
|
|
127
|
+
export function endSession(root, id, now = Date.now()) {
|
|
128
|
+
const had = readSession(root, id);
|
|
129
|
+
if (!had) return null;
|
|
130
|
+
return write(root, { ...had, ended: new Date(now).toISOString() });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** SESS-2: the person chose this session to lead: it does, and the session that led becomes a working one. */
|
|
134
|
+
export function makeLead(root, id, now = Date.now()) {
|
|
135
|
+
for (const s of sessions(root, now))
|
|
136
|
+
if (s.role === 'lead' && s.id !== id) write(root, { ...strip(s), role: 'working' });
|
|
137
|
+
const had = readSession(root, id) ?? startSession(root, id, now);
|
|
138
|
+
return write(root, { ...had, role: 'lead', seen: new Date(now).toISOString(), ended: null });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const strip = ({ live: _live, ...r }) => r;
|
|
142
|
+
|
|
143
|
+
/** SESS-4: the session's task: { title, paths, branch, started }, or null to end it. */
|
|
144
|
+
export function setTask(root, id, task, now = Date.now()) {
|
|
145
|
+
const had = readSession(root, id) ?? startSession(root, id, now);
|
|
146
|
+
return write(root, {
|
|
147
|
+
...had,
|
|
148
|
+
task: task ? { ...task, started: had.task?.started ?? new Date(now).toISOString() } : null,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* SESS-4: `taker` takes over the task of the session `from` (by its id or short id): the task, and its
|
|
154
|
+
* decisions with it (OWN-4), move to the session running the command. { ok, task, from } or { ok: false, why }.
|
|
155
|
+
*/
|
|
156
|
+
export function takeTask(root, from, taker, now = Date.now()) {
|
|
157
|
+
const source = sessions(root, now).find((s) => s.id === from || shortSession(s.id) === from);
|
|
158
|
+
if (!source?.task) return { ok: false, why: `No session ${from} has a task.` };
|
|
159
|
+
if (source.id === taker) return { ok: false, why: 'That task is already this session’s.' };
|
|
160
|
+
if (source.live)
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
why: `Session ${shortSession(source.id)} is still working on it; ask it to finish with \`code-kit task done\` first.`,
|
|
164
|
+
};
|
|
165
|
+
write(root, { ...strip(source), task: null });
|
|
166
|
+
const took = setTask(root, taker, source.task, now);
|
|
167
|
+
// OWN-4: the task's decisions come with it.
|
|
168
|
+
write(root, {
|
|
169
|
+
...took,
|
|
170
|
+
took: [...new Set([...(took.took ?? []), source.id, ...(source.took ?? [])])],
|
|
171
|
+
});
|
|
172
|
+
return { ok: true, task: source.task, from: source };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* SESS-4: why `session` may not write `rel`, because another live session's task leases it, or null.
|
|
177
|
+
* A stale task (its session has ended) leases nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function taskRefusal(root, session, rel, now = Date.now()) {
|
|
180
|
+
const other = sessions(root, now).find(
|
|
181
|
+
(s) => s.live && s.id !== session && s.task?.paths?.length && matchesAny(rel, s.task.paths),
|
|
182
|
+
);
|
|
183
|
+
if (!other) return null;
|
|
184
|
+
return `${rel} is leased by another session (${memberOf(other)}), working on "${other.task.title}". Leave it to that session, wait until it finishes (\`code-kit task done\` there), or work on something else.`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** SESS-5: a file a session wrote in a checkout, kept while it may still be uncommitted. */
|
|
188
|
+
export function noteWrite(root, id, rel, now = Date.now()) {
|
|
189
|
+
if (!id || !rel) return;
|
|
190
|
+
const had = readSession(root, id) ?? startSession(root, id, now);
|
|
191
|
+
const wrote = [rel, ...(had.wrote ?? []).filter((f) => f !== rel)].slice(0, WROTE_KEPT);
|
|
192
|
+
if (wrote.join('\n') === (had.wrote ?? []).join('\n')) return;
|
|
193
|
+
write(root, { ...had, wrote });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** SESS-5: the sessions, other than `session`, that wrote each of `files`: { file: [record] }. */
|
|
197
|
+
export function writersOf(root, files, session = null, now = Date.now()) {
|
|
198
|
+
const all = sessions(root, now).filter((s) => s.id !== session);
|
|
199
|
+
return Object.fromEntries(
|
|
200
|
+
files
|
|
201
|
+
.map((f) => [f, all.filter((s) => (s.wrote ?? []).includes(f))])
|
|
202
|
+
.filter(([, w]) => w.length),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* SESS-4, SESS-5: why this write may not happen because of the sessions working the project, or null.
|
|
208
|
+
* Another live session's task leases the path; or this is a working session's own main agent, which
|
|
209
|
+
* writes in a worktree of its own (the main checkout is every session's) and records its task first.
|
|
210
|
+
* code-kit's state, and paths outside the project, are never held back.
|
|
211
|
+
*/
|
|
212
|
+
export function sessionWriteProblem({ project, root, rel, actor, session, now = Date.now() }) {
|
|
213
|
+
if (!session || !rel || rel.startsWith('..') || rel.startsWith('.claude/state/')) return null;
|
|
214
|
+
const leased = taskRefusal(project, session, rel, now);
|
|
215
|
+
if (leased) return leased;
|
|
216
|
+
const me = readSession(project, session);
|
|
217
|
+
if (actor?.kind !== 'lead' || me?.role !== 'working') return null;
|
|
218
|
+
if (root === main(project))
|
|
219
|
+
return `this is a working session (another session leads the project), and ${rel} is in the main checkout, which every session shares: your uncommitted files there would block the other sessions' git. Work in a worktree of your own: start the session with \`claude --worktree\`, or move it into one (EnterWorktree), then record your task and go on.`;
|
|
220
|
+
if (!me.task)
|
|
221
|
+
return `this is a working session (another session leads the project): record what you're doing before you change code, so the other sessions see it and leave your files alone:\n node "${CLI}" task "<what you're doing, in a line>" --touches "<globs of the paths you'll change>"`;
|
|
222
|
+
return null;
|
|
223
|
+
}
|
package/hooks/lib/snapshot.mjs
CHANGED
|
@@ -14,8 +14,20 @@ import { sendBacks } from './reviews.mjs';
|
|
|
14
14
|
import { readMilestones, specCheckRows } from './plan.mjs';
|
|
15
15
|
import { storyTimes } from './timeline.mjs';
|
|
16
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';
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
import { slicesOf } from './schedule.mjs';
|
|
28
|
+
|
|
29
|
+
/** 2 adds the project's standards (code-kit spec 22); 3 the plan's slices (spec 23, SCHED-2). */
|
|
30
|
+
export const SNAPSHOT_VERSION = 3;
|
|
19
31
|
|
|
20
32
|
/** A story's stage, as a report names it. */
|
|
21
33
|
export const STAGES = [
|
|
@@ -42,6 +54,8 @@ export const SNAPSHOT_SCHEMA = {
|
|
|
42
54
|
version: 'count',
|
|
43
55
|
project: { repository: 'name?', base: 'name', codeKit: 'name', at: 'ms' },
|
|
44
56
|
milestones: [{ name: 'name', target: 'date?', stories: ['id'] }],
|
|
57
|
+
// SCHED-2: each slice in plan order, whether it's open or done, and its stories. Added in version 3.
|
|
58
|
+
slices: [{ name: 'name', open: 'bool', done: 'bool', stories: ['id'] }],
|
|
45
59
|
stories: [
|
|
46
60
|
{
|
|
47
61
|
id: 'id',
|
|
@@ -87,8 +101,28 @@ export const SNAPSHOT_SCHEMA = {
|
|
|
87
101
|
delegated: { protected: ['name'], dependencies: 'bool', merge: 'bool' },
|
|
88
102
|
designGate: 'bool',
|
|
89
103
|
},
|
|
104
|
+
// Spec 22: each standard that applies, by id and title, how it's applied and where it came from;
|
|
105
|
+
// its why in one line at most, and its evidence only as counts.
|
|
106
|
+
standards: [
|
|
107
|
+
{
|
|
108
|
+
id: 'id',
|
|
109
|
+
title: 'title',
|
|
110
|
+
category: `enum:${Object.values(CATEGORIES).join('|')}`,
|
|
111
|
+
level: `enum:${LEVELS.join('|')}`,
|
|
112
|
+
applied: `enum:${APPLIED.join('|')}`,
|
|
113
|
+
layers: ['name'],
|
|
114
|
+
source: `enum:${SOURCES.join('|')}`,
|
|
115
|
+
evidenceCount: 'count?',
|
|
116
|
+
evidenceOf: 'count?',
|
|
117
|
+
origin: `enum:${ORIGINS.join('|')}`,
|
|
118
|
+
why: 'line?',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
90
121
|
};
|
|
91
122
|
|
|
123
|
+
/** Parts a report from an older code-kit doesn't have yet: valid when missing, checked when there. */
|
|
124
|
+
export const LATER_PARTS = ['standards', 'slices'];
|
|
125
|
+
|
|
92
126
|
const ONE_LINE = (max) => (v) => typeof v === 'string' && v.length <= max && !/[\r\n]/.test(v);
|
|
93
127
|
const KINDS = {
|
|
94
128
|
id: (v) => typeof v === 'string' && /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(v),
|
|
@@ -122,7 +156,9 @@ export function snapshotProblems(value, shape = SNAPSHOT_SCHEMA, at = 'snapshot'
|
|
|
122
156
|
const extra = Object.keys(value).filter((k) => !(k in shape));
|
|
123
157
|
return [
|
|
124
158
|
...extra.map((k) => `${at}.${k}: not a field a report may hold`),
|
|
125
|
-
...Object.entries(shape)
|
|
159
|
+
...Object.entries(shape)
|
|
160
|
+
.filter(([k]) => !(shape === SNAPSHOT_SCHEMA && LATER_PARTS.includes(k) && !(k in value)))
|
|
161
|
+
.flatMap(([k, s]) => snapshotProblems(value[k], s, `${at}.${k}`)),
|
|
126
162
|
];
|
|
127
163
|
}
|
|
128
164
|
|
|
@@ -138,11 +174,13 @@ export function snapshotHash(snapshot, parts = null) {
|
|
|
138
174
|
/** The parts whose change is sent at once (HUBR-3); usage alone waits for the ten minutes. */
|
|
139
175
|
export const EVENT_PARTS = [
|
|
140
176
|
'milestones',
|
|
177
|
+
'slices',
|
|
141
178
|
'stories',
|
|
142
179
|
'sendBacks',
|
|
143
180
|
'requirements',
|
|
144
181
|
'approvals',
|
|
145
182
|
'rules',
|
|
183
|
+
'standards',
|
|
146
184
|
];
|
|
147
185
|
|
|
148
186
|
const git = (root, ...args) => {
|
|
@@ -330,6 +368,7 @@ export function buildSnapshot({
|
|
|
330
368
|
at: now,
|
|
331
369
|
},
|
|
332
370
|
milestones,
|
|
371
|
+
slices: slicesOf(status.stories).map((sl) => ({ ...sl, name: line(sl.name).slice(0, 120) })),
|
|
333
372
|
stories,
|
|
334
373
|
sendBacks: sendBackList,
|
|
335
374
|
requirements,
|
|
@@ -347,6 +386,45 @@ export function buildSnapshot({
|
|
|
347
386
|
},
|
|
348
387
|
designGate: Boolean(config.designGate),
|
|
349
388
|
},
|
|
389
|
+
standards: reportedStandards(root, config, line),
|
|
350
390
|
};
|
|
351
391
|
return snapshot;
|
|
352
392
|
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Spec 22: the standards that apply, as a report holds them. One that the file can't make sense of (an
|
|
396
|
+
* unknown category, no level) is left out: `code-kit standards` tells the project what's wrong with it.
|
|
397
|
+
*/
|
|
398
|
+
function reportedStandards(root, config, line) {
|
|
399
|
+
let all = [];
|
|
400
|
+
try {
|
|
401
|
+
all = projectStandards(root, config).standards;
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
return all
|
|
406
|
+
.filter(
|
|
407
|
+
(s) =>
|
|
408
|
+
KINDS.id(s.id) &&
|
|
409
|
+
CATEGORIES[s.id.split('-')[0]] &&
|
|
410
|
+
LEVELS.includes(s.level) &&
|
|
411
|
+
APPLIED.includes(s.applied) &&
|
|
412
|
+
KINDS.title(line(s.title) ?? ''),
|
|
413
|
+
)
|
|
414
|
+
.map((s) => {
|
|
415
|
+
const source = s.origin === 'inherited' ? { kind: 'organisation' } : sourceOf(s.source);
|
|
416
|
+
return {
|
|
417
|
+
id: s.id,
|
|
418
|
+
title: line(s.title),
|
|
419
|
+
category: CATEGORIES[s.id.split('-')[0]],
|
|
420
|
+
level: s.level,
|
|
421
|
+
applied: s.applied,
|
|
422
|
+
layers: (s.layers ?? []).filter((l) => KINDS.name(l)),
|
|
423
|
+
source: source.kind,
|
|
424
|
+
evidenceCount: source.count ?? null,
|
|
425
|
+
evidenceOf: source.of ?? null,
|
|
426
|
+
origin: ORIGINS.includes(s.origin) ? s.origin : 'own',
|
|
427
|
+
why: line(s.why),
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
}
|