@protoboxai/codeloop 0.3.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/LICENSE +21 -0
- package/README.md +589 -0
- package/dist/commands/adopt.d.ts +2 -0
- package/dist/commands/adopt.js +26 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/brief.d.ts +2 -0
- package/dist/commands/brief.js +22 -0
- package/dist/commands/brief.js.map +1 -0
- package/dist/commands/card.d.ts +6 -0
- package/dist/commands/card.js +159 -0
- package/dist/commands/card.js.map +1 -0
- package/dist/commands/check.d.ts +4 -0
- package/dist/commands/check.js +76 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/cloud.d.ts +8 -0
- package/dist/commands/cloud.js +123 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/guard.d.ts +2 -0
- package/dist/commands/guard.js +26 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/import.d.ts +2 -0
- package/dist/commands/import.js +24 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/inbox.d.ts +2 -0
- package/dist/commands/inbox.js +48 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +142 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +125 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/lane.d.ts +2 -0
- package/dist/commands/lane.js +100 -0
- package/dist/commands/lane.js.map +1 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +35 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +77 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/mock.d.ts +3 -0
- package/dist/commands/mock.js +22 -0
- package/dist/commands/mock.js.map +1 -0
- package/dist/commands/pack.d.ts +2 -0
- package/dist/commands/pack.js +19 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +125 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +31 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/render.d.ts +5 -0
- package/dist/commands/render.js +47 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +45 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +26 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +51 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +85 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/serve.d.ts +7 -0
- package/dist/commands/serve.js +126 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/spec.d.ts +3 -0
- package/dist/commands/spec.js +64 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +251 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +83 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +75 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +111 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/wiki.d.ts +3 -0
- package/dist/commands/wiki.js +85 -0
- package/dist/commands/wiki.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent.d.ts +27 -0
- package/dist/lib/agent.js +178 -0
- package/dist/lib/agent.js.map +1 -0
- package/dist/lib/board.d.ts +38 -0
- package/dist/lib/board.js +86 -0
- package/dist/lib/board.js.map +1 -0
- package/dist/lib/cards.d.ts +82 -0
- package/dist/lib/cards.js +107 -0
- package/dist/lib/cards.js.map +1 -0
- package/dist/lib/clock.d.ts +5 -0
- package/dist/lib/clock.js +14 -0
- package/dist/lib/clock.js.map +1 -0
- package/dist/lib/cloud.d.ts +122 -0
- package/dist/lib/cloud.js +580 -0
- package/dist/lib/cloud.js.map +1 -0
- package/dist/lib/competitors.d.ts +28 -0
- package/dist/lib/competitors.js +90 -0
- package/dist/lib/competitors.js.map +1 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +25 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/cron.d.ts +5 -0
- package/dist/lib/cron.js +82 -0
- package/dist/lib/cron.js.map +1 -0
- package/dist/lib/detect.d.ts +13 -0
- package/dist/lib/detect.js +60 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/engine.d.ts +88 -0
- package/dist/lib/engine.js +435 -0
- package/dist/lib/engine.js.map +1 -0
- package/dist/lib/flow.d.ts +44 -0
- package/dist/lib/flow.js +121 -0
- package/dist/lib/flow.js.map +1 -0
- package/dist/lib/glob.d.ts +2 -0
- package/dist/lib/glob.js +10 -0
- package/dist/lib/glob.js.map +1 -0
- package/dist/lib/import.d.ts +13 -0
- package/dist/lib/import.js +127 -0
- package/dist/lib/import.js.map +1 -0
- package/dist/lib/inbox.d.ts +42 -0
- package/dist/lib/inbox.js +65 -0
- package/dist/lib/inbox.js.map +1 -0
- package/dist/lib/lane.d.ts +71 -0
- package/dist/lib/lane.js +105 -0
- package/dist/lib/lane.js.map +1 -0
- package/dist/lib/lock.d.ts +6 -0
- package/dist/lib/lock.js +64 -0
- package/dist/lib/lock.js.map +1 -0
- package/dist/lib/mcp.d.ts +2 -0
- package/dist/lib/mcp.js +57 -0
- package/dist/lib/mcp.js.map +1 -0
- package/dist/lib/mock.d.ts +17 -0
- package/dist/lib/mock.js +137 -0
- package/dist/lib/mock.js.map +1 -0
- package/dist/lib/pack.d.ts +24 -0
- package/dist/lib/pack.js +77 -0
- package/dist/lib/pack.js.map +1 -0
- package/dist/lib/proposals.d.ts +48 -0
- package/dist/lib/proposals.js +240 -0
- package/dist/lib/proposals.js.map +1 -0
- package/dist/lib/render.d.ts +7 -0
- package/dist/lib/render.js +84 -0
- package/dist/lib/render.js.map +1 -0
- package/dist/lib/research.d.ts +8 -0
- package/dist/lib/research.js +39 -0
- package/dist/lib/research.js.map +1 -0
- package/dist/lib/run.d.ts +38 -0
- package/dist/lib/run.js +215 -0
- package/dist/lib/run.js.map +1 -0
- package/dist/lib/scaffold.d.ts +16 -0
- package/dist/lib/scaffold.js +175 -0
- package/dist/lib/scaffold.js.map +1 -0
- package/dist/lib/scan.d.ts +22 -0
- package/dist/lib/scan.js +85 -0
- package/dist/lib/scan.js.map +1 -0
- package/dist/lib/schedule.d.ts +20 -0
- package/dist/lib/schedule.js +66 -0
- package/dist/lib/schedule.js.map +1 -0
- package/dist/lib/server.d.ts +68 -0
- package/dist/lib/server.js +235 -0
- package/dist/lib/server.js.map +1 -0
- package/dist/lib/shell.d.ts +1 -0
- package/dist/lib/shell.js +25 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.js +92 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/spec.d.ts +24 -0
- package/dist/lib/spec.js +102 -0
- package/dist/lib/spec.js.map +1 -0
- package/dist/lib/stats.d.ts +13 -0
- package/dist/lib/stats.js +43 -0
- package/dist/lib/stats.js.map +1 -0
- package/dist/lib/verify.d.ts +25 -0
- package/dist/lib/verify.js +135 -0
- package/dist/lib/verify.js.map +1 -0
- package/dist/lib/version.d.ts +10 -0
- package/dist/lib/version.js +27 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/wiki.d.ts +51 -0
- package/dist/lib/wiki.js +164 -0
- package/dist/lib/wiki.js.map +1 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/installer.d.ts +30 -0
- package/dist/registry/installer.js +133 -0
- package/dist/registry/installer.js.map +1 -0
- package/dist/registry/local-index.d.ts +32 -0
- package/dist/registry/local-index.js +58 -0
- package/dist/registry/local-index.js.map +1 -0
- package/dist/registry/lockfile.d.ts +40 -0
- package/dist/registry/lockfile.js +85 -0
- package/dist/registry/lockfile.js.map +1 -0
- package/dist/registry/security.d.ts +25 -0
- package/dist/registry/security.js +100 -0
- package/dist/registry/security.js.map +1 -0
- package/dist/registry/skill-schema.d.ts +30 -0
- package/dist/registry/skill-schema.js +95 -0
- package/dist/registry/skill-schema.js.map +1 -0
- package/dist/ui/404.html +1 -0
- package/dist/ui/_next/static/6wzGE5sCtbYkSyhMWxL32/_buildManifest.js +1 -0
- package/dist/ui/_next/static/6wzGE5sCtbYkSyhMWxL32/_ssgManifest.js +1 -0
- package/dist/ui/_next/static/chunks/255-54d3085ce94738a4.js +1 -0
- package/dist/ui/_next/static/chunks/423-bb541b7ae2733575.js +1 -0
- package/dist/ui/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
- package/dist/ui/_next/static/chunks/app/_not-found/page-d6bc774f7acb716e.js +1 -0
- package/dist/ui/_next/static/chunks/app/layout-e5fc8e78e1c8da95.js +1 -0
- package/dist/ui/_next/static/chunks/app/page-0c34b6e119cef236.js +1 -0
- package/dist/ui/_next/static/chunks/framework-de98b93a850cfc71.js +1 -0
- package/dist/ui/_next/static/chunks/main-49fd204fc9037ea3.js +1 -0
- package/dist/ui/_next/static/chunks/main-app-c46afa2f48f3aaef.js +1 -0
- package/dist/ui/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/dist/ui/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/dist/ui/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/ui/_next/static/chunks/webpack-4a462cecab786e93.js +1 -0
- package/dist/ui/_next/static/css/1bf01240dbfd6088.css +1 -0
- package/dist/ui/index.html +1 -0
- package/dist/ui/index.txt +19 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +88 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/reporter.d.ts +11 -0
- package/dist/watch/reporter.js +44 -0
- package/dist/watch/reporter.js.map +1 -0
- package/dist/watch/signals.d.ts +38 -0
- package/dist/watch/signals.js +119 -0
- package/dist/watch/signals.js.map +1 -0
- package/dist/watch/triggers.d.ts +10 -0
- package/dist/watch/triggers.js +67 -0
- package/dist/watch/triggers.js.map +1 -0
- package/package.json +64 -0
- package/registry/index.json +106 -0
- package/starters/generic.yaml +95 -0
- package/starters/go.yaml +99 -0
- package/starters/node-typescript.yaml +108 -0
- package/starters/python.yaml +105 -0
- package/templates/ci/codeloop-pr.yml +29 -0
- package/templates/ci/codeloop-prod.yml +27 -0
- package/templates/ci/codeloop-staging.yml +34 -0
- package/templates/codeloop/board.json +5 -0
- package/templates/codeloop/gotchas.md +13 -0
- package/templates/codeloop/patterns.md +15 -0
- package/templates/codeloop/principles.md +49 -0
- package/templates/codeloop/rules.md +23 -0
- package/templates/commands/commit.md +255 -0
- package/templates/commands/debug.md +142 -0
- package/templates/commands/deploy.md +144 -0
- package/templates/commands/design.md +102 -0
- package/templates/commands/manage.md +77 -0
- package/templates/commands/plan.md +84 -0
- package/templates/commands/qa.md +155 -0
- package/templates/commands/reflect.md +93 -0
- package/templates/commands/ship.md +187 -0
- package/templates/commands/test.md +133 -0
- package/templates/hooks/commit-msg +8 -0
- package/templates/lanes/analyze.yaml +25 -0
- package/templates/lanes/build.yaml +45 -0
- package/templates/lanes/deploy.yaml +24 -0
- package/templates/lanes/learn.yaml +20 -0
- package/templates/lanes/market.yaml +25 -0
- package/templates/lanes/plan.yaml +24 -0
- package/templates/lanes/scan.yaml +11 -0
- package/templates/lanes/triage.yaml +24 -0
- package/templates/mock/template.html +82 -0
- package/templates/seeds/go-gotchas.md +28 -0
- package/templates/seeds/go-patterns.md +22 -0
- package/templates/seeds/node-typescript-gotchas.md +30 -0
- package/templates/seeds/node-typescript-patterns.md +27 -0
- package/templates/seeds/python-gotchas.md +30 -0
- package/templates/seeds/python-patterns.md +19 -0
- package/templates/seeds/universal-gotchas.md +11 -0
- package/templates/seeds/universal-patterns.md +11 -0
- package/templates/spec/plan.md +7 -0
- package/templates/spec/research.md +19 -0
- package/templates/spec/spec.md +14 -0
- package/templates/spec/tasks.md +10 -0
- package/templates/tasks/todo.md +3 -0
package/dist/lib/spec.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { cardKey, findCard, findCardOrNone, readCards } from './cards.js';
|
|
5
|
+
import { annotateCard, RefusalError } from './engine.js';
|
|
6
|
+
import { cardNumber } from './lane.js';
|
|
7
|
+
import { withLock } from './lock.js';
|
|
8
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
9
|
+
export const LAYERS = ['api', 'sdk', 'ui', 'test', 'docs'];
|
|
10
|
+
const MAX_ACCEPTANCE = 5;
|
|
11
|
+
const TASK = /^- \[( |x)\] (T\d{3})( \[P\])?( \[(US\d+)\])? \[(api|sdk|ui|test|docs)\] (.+)$/;
|
|
12
|
+
const ACCEPTANCE = /^- (US\d+)[ :.]/;
|
|
13
|
+
export function slugify(text) {
|
|
14
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '');
|
|
15
|
+
}
|
|
16
|
+
/** `ref` is a card id or its number. A folder recorded on the card wins over a folder found by number. */
|
|
17
|
+
export function resolveSpecDir(projectDir, ref) {
|
|
18
|
+
const card = findCardOrNone(readCards(projectDir).cards, ref);
|
|
19
|
+
if (card?.spec)
|
|
20
|
+
return card.spec;
|
|
21
|
+
const key = card ? cardKey(card) : cardNumber(ref);
|
|
22
|
+
if (existsSync(join(projectDir, 'specs', key)))
|
|
23
|
+
return `specs/${key}`;
|
|
24
|
+
const specs = join(projectDir, 'specs');
|
|
25
|
+
const found = existsSync(specs) ? readdirSync(specs).filter(d => d.startsWith(`${key}-`)) : [];
|
|
26
|
+
if (found.length === 1)
|
|
27
|
+
return `specs/${found[0]}`;
|
|
28
|
+
throw new RefusalError(found.length ? `specs/${key}-* matches ${found.length} folders; pass the card id` : `no spec folder for ${ref}; run \`codeloop spec new <card-id>\``);
|
|
29
|
+
}
|
|
30
|
+
export function newSpec(projectDir, cardId, role = 'agent') {
|
|
31
|
+
const card = findCard(readCards(projectDir).cards, cardId);
|
|
32
|
+
const dir = card.spec ?? `specs/${cardKey(card)}-${slugify(card.title)}`;
|
|
33
|
+
const created = [];
|
|
34
|
+
mkdirSync(join(projectDir, dir), { recursive: true });
|
|
35
|
+
for (const name of ['research.md', 'spec.md', 'plan.md', 'tasks.md']) {
|
|
36
|
+
const dest = join(projectDir, dir, name);
|
|
37
|
+
if (existsSync(dest))
|
|
38
|
+
continue;
|
|
39
|
+
const template = readFileSync(join(PACKAGE_ROOT, 'templates/spec', name), 'utf-8');
|
|
40
|
+
writeFileSync(dest, template.replaceAll('{{id}}', card.id).replaceAll('{{title}}', card.title));
|
|
41
|
+
created.push(`${dir}/${name}`);
|
|
42
|
+
}
|
|
43
|
+
if (card.spec !== dir)
|
|
44
|
+
annotateCard(projectDir, card.id, { spec: dir }, 'spec', { note: dir, role });
|
|
45
|
+
return { dir, created };
|
|
46
|
+
}
|
|
47
|
+
function read(projectDir, dir, name) {
|
|
48
|
+
const file = join(projectDir, dir, name);
|
|
49
|
+
if (!existsSync(file))
|
|
50
|
+
throw new RefusalError(`${dir}/${name} is missing`);
|
|
51
|
+
return readFileSync(file, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
export function readTasks(projectDir, dir) {
|
|
54
|
+
const tasks = [];
|
|
55
|
+
const malformed = [];
|
|
56
|
+
for (const line of read(projectDir, dir, 'tasks.md').split('\n')) {
|
|
57
|
+
if (!/^- \[[ x]\]/.test(line))
|
|
58
|
+
continue;
|
|
59
|
+
const m = TASK.exec(line);
|
|
60
|
+
if (m)
|
|
61
|
+
tasks.push({ done: m[1] === 'x', id: m[2], parallel: !!m[3], accept: m[5], layer: m[6], text: m[7] });
|
|
62
|
+
else
|
|
63
|
+
malformed.push(line);
|
|
64
|
+
}
|
|
65
|
+
return { tasks, malformed };
|
|
66
|
+
}
|
|
67
|
+
export function readAcceptance(projectDir, dir) {
|
|
68
|
+
return read(projectDir, dir, 'spec.md').split('\n').map(l => ACCEPTANCE.exec(l)?.[1]).filter((id) => !!id);
|
|
69
|
+
}
|
|
70
|
+
export function checkSpec(projectDir, dir) {
|
|
71
|
+
const acceptance = readAcceptance(projectDir, dir);
|
|
72
|
+
const { tasks, malformed } = readTasks(projectDir, dir);
|
|
73
|
+
const errors = [];
|
|
74
|
+
if (acceptance.length === 0)
|
|
75
|
+
errors.push(`${dir}/spec.md has no acceptance lines (\`- US1 Given ...\`)`);
|
|
76
|
+
if (acceptance.length > MAX_ACCEPTANCE)
|
|
77
|
+
errors.push(`${acceptance.length} acceptance lines, the limit is ${MAX_ACCEPTANCE}: split the card`);
|
|
78
|
+
for (const line of malformed)
|
|
79
|
+
errors.push(`untagged task (needs a T-number and one of [${LAYERS.join('|')}]): ${line}`);
|
|
80
|
+
for (const us of acceptance) {
|
|
81
|
+
if (!tasks.some(t => t.accept === us))
|
|
82
|
+
errors.push(`acceptance line ${us} has no task`);
|
|
83
|
+
}
|
|
84
|
+
for (const task of tasks) {
|
|
85
|
+
if (task.accept && !acceptance.includes(task.accept))
|
|
86
|
+
errors.push(`task ${task.id} cites ${task.accept}, which spec.md does not have`);
|
|
87
|
+
}
|
|
88
|
+
return errors;
|
|
89
|
+
}
|
|
90
|
+
export function tickTask(projectDir, dir, taskId) {
|
|
91
|
+
const file = join(projectDir, dir, 'tasks.md');
|
|
92
|
+
// Builders on different layers tick tasks in the same file at the same time.
|
|
93
|
+
withLock(file, () => {
|
|
94
|
+
const lines = read(projectDir, dir, 'tasks.md').split('\n');
|
|
95
|
+
const index = lines.findIndex(l => TASK.exec(l)?.[2] === taskId);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
throw new RefusalError(`task ${taskId} not found in ${dir}/tasks.md`);
|
|
98
|
+
lines[index] = lines[index].replace('- [ ]', '- [x]');
|
|
99
|
+
writeFileSync(file, lines.join('\n'));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/lib/spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Card } from './cards.js';
|
|
2
|
+
import type { Lane } from './lane.js';
|
|
3
|
+
export interface Stats {
|
|
4
|
+
cards: number;
|
|
5
|
+
done: number;
|
|
6
|
+
human_turns_per_card: number | null;
|
|
7
|
+
unattended_span_hours: number | null;
|
|
8
|
+
cycle_time_hours: number | null;
|
|
9
|
+
rework: number | null;
|
|
10
|
+
stuck_rate: number | null;
|
|
11
|
+
first_pass_rate_by_gate: Record<string, number>;
|
|
12
|
+
}
|
|
13
|
+
export declare function computeStats(cards: Card[], lanes: Lane[]): Stats;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DONE, PROPOSED } from './cards.js';
|
|
2
|
+
const HOUR = 3_600_000;
|
|
3
|
+
const ms = (iso) => new Date(iso).getTime();
|
|
4
|
+
const ratio = (n, d) => (d ? n / d : null);
|
|
5
|
+
// Longest stretch of a card's life with no human event: creation, each human event, last event.
|
|
6
|
+
function unattended(card) {
|
|
7
|
+
const marks = [card.createdAt, ...card.events.filter(e => e.human).map(e => e.at), card.events.at(-1)?.at ?? card.createdAt].map(ms);
|
|
8
|
+
return Math.max(...marks.slice(1).map((t, i) => t - marks[i]), 0);
|
|
9
|
+
}
|
|
10
|
+
export function computeStats(cards, lanes) {
|
|
11
|
+
// Dropping a proposal is a choice about what to work on, not a judgement of work, so it is
|
|
12
|
+
// left out of the gate and rework numbers. It still counts as a human turn.
|
|
13
|
+
const judged = (e) => e.stage !== PROPOSED;
|
|
14
|
+
const events = cards.flatMap(c => c.events);
|
|
15
|
+
const done = cards.filter(c => c.stage === DONE);
|
|
16
|
+
// First pass at a gate: the first human decision at that stage was an approval.
|
|
17
|
+
const gates = {};
|
|
18
|
+
for (const card of cards) {
|
|
19
|
+
const lane = lanes.find(l => l.id === card.lane);
|
|
20
|
+
const decided = new Set();
|
|
21
|
+
for (const e of card.events) {
|
|
22
|
+
if ((e.action !== 'approve' && e.action !== 'reject') || !e.stage || !judged(e) || decided.has(e.stage))
|
|
23
|
+
continue;
|
|
24
|
+
decided.add(e.stage);
|
|
25
|
+
const key = `${card.lane}/${lane?.stages.find(s => s.id === e.stage)?.gate?.name ?? e.stage}`;
|
|
26
|
+
gates[key] ??= { first: 0, total: 0 };
|
|
27
|
+
gates[key].total++;
|
|
28
|
+
if (e.action === 'approve')
|
|
29
|
+
gates[key].first++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
cards: cards.length,
|
|
34
|
+
done: done.length,
|
|
35
|
+
human_turns_per_card: ratio(events.filter(e => e.human).length, cards.length),
|
|
36
|
+
unattended_span_hours: cards.length ? Math.max(...cards.map(unattended)) / HOUR : null,
|
|
37
|
+
cycle_time_hours: done.length ? done.reduce((sum, c) => sum + ms(c.updatedAt) - ms(c.createdAt), 0) / done.length / HOUR : null,
|
|
38
|
+
rework: ratio(events.filter(e => e.action === 'reject' && judged(e)).length, events.filter(e => e.action === 'approve').length),
|
|
39
|
+
stuck_rate: ratio(cards.filter(c => c.events.some(e => e.action === 'stuck')).length, cards.length),
|
|
40
|
+
first_pass_rate_by_gate: Object.fromEntries(Object.entries(gates).map(([k, g]) => [k, g.first / g.total])),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/lib/stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAa,MAAM,YAAY,CAAC;AAcvD,MAAM,IAAI,GAAG,SAAS,CAAC;AACvB,MAAM,EAAE,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;AACpD,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAE3D,gGAAgG;AAChG,SAAS,UAAU,CAAC,IAAU;IAC5B,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrI,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,KAAa;IACvD,2FAA2F;IAC3F,4EAA4E;IAC5E,MAAM,MAAM,GAAG,CAAC,CAAqB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC;IAC/D,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;IAEjD,gFAAgF;IAChF,MAAM,KAAK,GAAqD,EAAE,CAAC;IACnE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAE,SAAS;YAClH,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;YAC9F,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YACtC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS;gBAAE,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;QACjD,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,MAAM;QACnB,IAAI,EAAE,IAAI,CAAC,MAAM;QACjB,oBAAoB,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;QAC7E,qBAAqB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI;QACtF,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI;QAC/H,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;QAC/H,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;QACnG,uBAAuB,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;KAC3G,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface LayerResult {
|
|
2
|
+
layer: string;
|
|
3
|
+
pass: boolean;
|
|
4
|
+
output: string;
|
|
5
|
+
}
|
|
6
|
+
export interface VerifyResult {
|
|
7
|
+
exitCode: number;
|
|
8
|
+
sha: string;
|
|
9
|
+
missing: string[];
|
|
10
|
+
cases: {
|
|
11
|
+
id: string;
|
|
12
|
+
accept: string;
|
|
13
|
+
pass: boolean;
|
|
14
|
+
layers: LayerResult[];
|
|
15
|
+
}[];
|
|
16
|
+
vacuous: string[];
|
|
17
|
+
mutate?: string;
|
|
18
|
+
/** Why nothing was run. */
|
|
19
|
+
problem?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function verify(projectDir: string, ref: string, opts?: {
|
|
22
|
+
mutate?: boolean;
|
|
23
|
+
env?: string;
|
|
24
|
+
record?: boolean;
|
|
25
|
+
}): Promise<VerifyResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { cardKey, findCardOrNone, readCards, safeName } from './cards.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
8
|
+
import { annotateCard, RefusalError } from './engine.js';
|
|
9
|
+
import { cardNumber } from './lane.js';
|
|
10
|
+
import { checkEnv } from './shell.js';
|
|
11
|
+
import { readAcceptance, resolveSpecDir } from './spec.js';
|
|
12
|
+
const tail = (text) => text.trimEnd().split('\n').slice(-20).join('\n').slice(-2000);
|
|
13
|
+
function git(cwd, args) {
|
|
14
|
+
const run = spawnSync('git', args, { cwd, encoding: 'utf-8' });
|
|
15
|
+
return run.status === 0 ? run.stdout.trim() : '';
|
|
16
|
+
}
|
|
17
|
+
function loadUseCases(projectDir, nnn) {
|
|
18
|
+
const dir = join(projectDir, 'usecases', nnn);
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
return [];
|
|
21
|
+
return readdirSync(dir).filter(f => f.endsWith('.yaml')).sort().map(f => {
|
|
22
|
+
const uc = parseYaml(readFileSync(join(dir, f), 'utf-8'));
|
|
23
|
+
// The id names the evidence file.
|
|
24
|
+
safeName(uc?.id, `use case id in usecases/${nnn}/${f}`);
|
|
25
|
+
return uc;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function runCli(uc, cwd, env, base = checkEnv()) {
|
|
29
|
+
const { run, expect } = uc.layers.cli;
|
|
30
|
+
const proc = spawnSync(run, { cwd, shell: true, encoding: 'utf-8', timeout: 600_000, env: { ...base, ...(env ? { CODELOOP_ENV: env } : {}) } });
|
|
31
|
+
const pass = proc.status === (expect?.exit ?? 0) && (!expect?.stdout || (proc.stdout ?? '').includes(expect.stdout));
|
|
32
|
+
return { layer: 'cli', pass, output: tail(`${proc.stdout ?? ''}${proc.stderr ?? ''}\nexit ${proc.status}`) };
|
|
33
|
+
}
|
|
34
|
+
async function runApi(uc, baseUrl) {
|
|
35
|
+
const { request, expect } = uc.layers.api;
|
|
36
|
+
const url = `${(uc.layers.api.base_url ?? baseUrl).replace(/\/$/, '')}${request.path}`;
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
method: request.method ?? 'GET',
|
|
40
|
+
...(request.body !== undefined ? { body: JSON.stringify(request.body), headers: { 'content-type': 'application/json' } } : {}),
|
|
41
|
+
});
|
|
42
|
+
return { layer: 'api', pass: res.status === expect.status, output: tail(`${request.method ?? 'GET'} ${url} → ${res.status} (expected ${expect.status})\n${await res.text()}`) };
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
return { layer: 'api', pass: false, output: `${request.method ?? 'GET'} ${url} failed: ${e.message}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function baseUrlFor(projectDir, env) {
|
|
49
|
+
const config = loadConfig(projectDir);
|
|
50
|
+
return (env && config.deploy?.[env]?.base_url) || process.env.CODELOOP_BASE_URL || 'http://localhost:4040';
|
|
51
|
+
}
|
|
52
|
+
function setupCommand(projectDir, tree) {
|
|
53
|
+
const configured = loadConfig(projectDir).verify?.setup;
|
|
54
|
+
if (configured !== undefined)
|
|
55
|
+
return configured || '';
|
|
56
|
+
return existsSync(join(tree, 'package.json')) ? 'npm ci && npm run build --if-present' : '';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Re-runs the cli use cases on the commit before the card's first `Feature: <id>` commit. A use
|
|
60
|
+
* case that still passes there never depended on the feature, so it proves nothing. The worktree
|
|
61
|
+
* is installed and built first: a use case that fails there only because nothing was built would
|
|
62
|
+
* look non-vacuous without being so.
|
|
63
|
+
*/
|
|
64
|
+
function mutate(projectDir, cardId, cases) {
|
|
65
|
+
const first = git(projectDir, ['log', '--reverse', '--format=%H', `--grep=^Feature: ${cardId}$`]).split('\n')[0];
|
|
66
|
+
if (!first)
|
|
67
|
+
return { vacuous: [], cannotRun: true, message: `cannot run: no commit carries the trailer "Feature: ${cardId}", so there is no commit before the feature to compare against` };
|
|
68
|
+
const base = git(projectDir, ['rev-parse', `${first}^`]);
|
|
69
|
+
if (!base)
|
|
70
|
+
return { vacuous: [], cannotRun: true, message: `cannot run: the first Feature: ${cardId} commit has no parent` };
|
|
71
|
+
const tree = join(mkdtempSync(join(tmpdir(), 'codeloop-mutate-')), 'tree');
|
|
72
|
+
if (spawnSync('git', ['worktree', 'add', '--detach', tree, base], { cwd: projectDir, encoding: 'utf-8' }).status !== 0) {
|
|
73
|
+
throw new RefusalError(`could not create a worktree at ${base.slice(0, 7)}`);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const setup = setupCommand(projectDir, tree);
|
|
77
|
+
if (setup) {
|
|
78
|
+
const run = spawnSync(setup, { cwd: tree, shell: true, encoding: 'utf-8', timeout: 900_000 });
|
|
79
|
+
if (run.status !== 0) {
|
|
80
|
+
return { vacuous: [], cannotRun: true, message: `environment differs: \`${setup}\` failed at ${base.slice(0, 7)}, so the use cases were not judged there\n${tail(`${run.stdout ?? ''}${run.stderr ?? ''}`)}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// When the project ships the `codeloop` binary itself, use cases must drive the old build, not
|
|
84
|
+
// the one running this command.
|
|
85
|
+
const bin = existsSync(join(tree, 'package.json')) ? JSON.parse(readFileSync(join(tree, 'package.json'), 'utf-8')).bin?.codeloop : undefined;
|
|
86
|
+
const env = bin && existsSync(join(tree, bin)) ? checkEnv(join(tree, bin)) : checkEnv();
|
|
87
|
+
const vacuous = cases.filter(uc => uc.layers.cli && runCli(uc, tree, undefined, env).pass).map(uc => uc.id);
|
|
88
|
+
return { vacuous, message: `re-ran ${cases.filter(uc => uc.layers.cli).length} cli use cases at ${base.slice(0, 7)}, before ${cardId}${setup ? ` (after \`${setup}\`)` : ''}` };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
spawnSync('git', ['worktree', 'remove', '--force', tree], { cwd: projectDir });
|
|
92
|
+
rmSync(join(tree, '..'), { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function verify(projectDir, ref, opts = {}) {
|
|
96
|
+
const card = findCardOrNone(readCards(projectDir).cards, ref);
|
|
97
|
+
const nnn = card ? cardKey(card) : cardNumber(ref);
|
|
98
|
+
const acceptance = readAcceptance(projectDir, resolveSpecDir(projectDir, ref));
|
|
99
|
+
const cases = loadUseCases(projectDir, nnn);
|
|
100
|
+
const sha = git(projectDir, ['rev-parse', 'HEAD']);
|
|
101
|
+
const result = { exitCode: 0, sha, missing: acceptance.filter(us => !cases.some(uc => uc.accept === us)), cases: [], vacuous: [] };
|
|
102
|
+
// Nothing to check is not a pass: no acceptance lines or no use cases must stop the stage.
|
|
103
|
+
if (acceptance.length === 0)
|
|
104
|
+
return { ...result, exitCode: 2, problem: `the spec for ${ref} has no acceptance lines (\`- US1 Given ...\`), so there is nothing to verify` };
|
|
105
|
+
if (result.missing.length)
|
|
106
|
+
return { ...result, exitCode: 2 };
|
|
107
|
+
const baseUrl = baseUrlFor(projectDir, opts.env);
|
|
108
|
+
const out = join(projectDir, 'evidence', nnn);
|
|
109
|
+
mkdirSync(out, { recursive: true });
|
|
110
|
+
for (const uc of cases) {
|
|
111
|
+
const layers = [];
|
|
112
|
+
if (uc.layers.cli)
|
|
113
|
+
layers.push(runCli(uc, projectDir, opts.env));
|
|
114
|
+
if (uc.layers.api)
|
|
115
|
+
layers.push(await runApi(uc, baseUrl));
|
|
116
|
+
// A use case with nothing runnable (ui only, or empty) must not count as a pass.
|
|
117
|
+
const pass = layers.length > 0 && layers.every(l => l.pass);
|
|
118
|
+
result.cases.push({ id: uc.id, accept: uc.accept, pass, layers });
|
|
119
|
+
writeFileSync(join(out, `${uc.id}.json`), JSON.stringify({ id: uc.id, accept: uc.accept, failure_mode: uc.failure_mode, sha, env: opts.env ?? 'local', pass, layers }, null, 2) + '\n');
|
|
120
|
+
}
|
|
121
|
+
const passed = result.cases.every(c => c.pass);
|
|
122
|
+
const rows = result.cases.map(c => `| ${c.id} | ${c.accept} | ${c.layers.map(l => `${l.layer} ${l.pass ? 'pass' : 'FAIL'}`).join(', ') || 'no runnable layer'} |`);
|
|
123
|
+
writeFileSync(join(out, 'verify.md'), [`# Verify ${ref}`, '', `result: ${passed ? 'pass' : 'fail'}`, `sha: ${sha || 'none'}`, `env: ${opts.env ?? 'local'}`, '', '| Use case | Accepts | Layers |', '|---|---|---|', ...rows, ''].join('\n'));
|
|
124
|
+
if (card && opts.record !== false) {
|
|
125
|
+
annotateCard(projectDir, card.id, { evidence: `evidence/${nnn}/verify.md` }, 'verify', { note: `result: ${passed ? 'pass' : 'fail'} at ${sha.slice(0, 7) || 'no commit'}` });
|
|
126
|
+
}
|
|
127
|
+
if (!passed)
|
|
128
|
+
return { ...result, exitCode: 1 };
|
|
129
|
+
if (opts.mutate) {
|
|
130
|
+
const m = mutate(projectDir, card?.id ?? ref, cases);
|
|
131
|
+
return { ...result, vacuous: m.vacuous, mutate: m.message, exitCode: m.cannotRun ? 5 : m.vacuous.length ? 4 : 0 };
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/lib/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two semver-style version strings.
|
|
3
|
+
* Returns: -1 if a < b, 0 if equal, 1 if a > b
|
|
4
|
+
*/
|
|
5
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
6
|
+
/**
|
|
7
|
+
* Parse version from a codeloop-version comment in file content.
|
|
8
|
+
* Also supports legacy flywheel-version for backward compatibility.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseVersion(content: string): string | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two semver-style version strings.
|
|
3
|
+
* Returns: -1 if a < b, 0 if equal, 1 if a > b
|
|
4
|
+
*/
|
|
5
|
+
export function compareVersions(a, b) {
|
|
6
|
+
const partsA = a.split('.').map(Number);
|
|
7
|
+
const partsB = b.split('.').map(Number);
|
|
8
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
9
|
+
for (let i = 0; i < len; i++) {
|
|
10
|
+
const numA = partsA[i] ?? 0;
|
|
11
|
+
const numB = partsB[i] ?? 0;
|
|
12
|
+
if (numA < numB)
|
|
13
|
+
return -1;
|
|
14
|
+
if (numA > numB)
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse version from a codeloop-version comment in file content.
|
|
21
|
+
* Also supports legacy flywheel-version for backward compatibility.
|
|
22
|
+
*/
|
|
23
|
+
export function parseVersion(content) {
|
|
24
|
+
const match = content.match(/<!--\s*(?:codeloop|flywheel)-version:\s*([\d.]+)\s*-->/);
|
|
25
|
+
return match ? match[1] : null;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS,EAAE,CAAS;IAClD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAEnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,IAAI,GAAG,IAAI;YAAE,OAAO,CAAC,CAAC,CAAC;QAC3B,IAAI,IAAI,GAAG,IAAI;YAAE,OAAO,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const WIKI_DIR = ".codeloop/wiki";
|
|
2
|
+
declare const KINDS: {
|
|
3
|
+
readonly gotcha: "gotchas";
|
|
4
|
+
readonly decision: "decisions";
|
|
5
|
+
readonly concept: "concepts";
|
|
6
|
+
};
|
|
7
|
+
export type Kind = keyof typeof KINDS;
|
|
8
|
+
export interface Page {
|
|
9
|
+
path: string;
|
|
10
|
+
kind: Kind;
|
|
11
|
+
title: string;
|
|
12
|
+
scope: string[];
|
|
13
|
+
freq: number;
|
|
14
|
+
severity: 'warning' | 'critical';
|
|
15
|
+
cards: string[];
|
|
16
|
+
updated: string;
|
|
17
|
+
promoted?: boolean;
|
|
18
|
+
body: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function listPages(projectDir: string): Page[];
|
|
21
|
+
/** Pages that changed here and in the cloud: the local file and the cloud version written beside it, waiting for a merge. */
|
|
22
|
+
export declare function cloudCopies(projectDir: string): {
|
|
23
|
+
path: string;
|
|
24
|
+
cloud: string;
|
|
25
|
+
}[];
|
|
26
|
+
export declare function thresholds(projectDir: string): {
|
|
27
|
+
critical: number;
|
|
28
|
+
promote: number;
|
|
29
|
+
};
|
|
30
|
+
/** A title that already exists is the same lesson met again: its frequency goes up instead of a second page. */
|
|
31
|
+
export declare function capture(projectDir: string, input: {
|
|
32
|
+
title: string;
|
|
33
|
+
scope: string[];
|
|
34
|
+
body: string;
|
|
35
|
+
kind?: Kind;
|
|
36
|
+
card?: string;
|
|
37
|
+
}, now?: Date): Page;
|
|
38
|
+
export declare function inject(projectDir: string, files: string[]): Page[];
|
|
39
|
+
/** Applies the frequency thresholds: critical at `critical_frequency`, copied into rules.md once at `promote_frequency`. */
|
|
40
|
+
export declare function learn(projectDir: string): {
|
|
41
|
+
critical: string[];
|
|
42
|
+
promoted: string[];
|
|
43
|
+
};
|
|
44
|
+
export declare function lintWiki(projectDir: string, now?: Date): {
|
|
45
|
+
broken: string[];
|
|
46
|
+
stale: string[];
|
|
47
|
+
duplicates: string[];
|
|
48
|
+
};
|
|
49
|
+
/** Critical pages whose scope matches the files and that were not acknowledged by title. */
|
|
50
|
+
export declare function blockingGotchas(projectDir: string, files: string[], acked: string[]): Page[];
|
|
51
|
+
export {};
|
package/dist/lib/wiki.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
3
|
+
import { stringify } from 'yaml';
|
|
4
|
+
import { CLOUD_COPY, writeCloudWikiPage } from './cloud.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { RefusalError } from './engine.js';
|
|
7
|
+
import { globMatches } from './glob.js';
|
|
8
|
+
import { clock } from './clock.js';
|
|
9
|
+
import { withLock } from './lock.js';
|
|
10
|
+
import { parseFrontmatter } from './skills.js';
|
|
11
|
+
import { slugify } from './spec.js';
|
|
12
|
+
export const WIKI_DIR = '.codeloop/wiki';
|
|
13
|
+
const KINDS = { gotcha: 'gotchas', decision: 'decisions', concept: 'concepts' };
|
|
14
|
+
const STALE_DAYS = 90;
|
|
15
|
+
export function listPages(projectDir) {
|
|
16
|
+
const pages = [];
|
|
17
|
+
for (const [kind, folder] of Object.entries(KINDS)) {
|
|
18
|
+
const dir = join(projectDir, WIKI_DIR, folder);
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
continue;
|
|
21
|
+
for (const file of readdirSync(dir).filter(f => f.endsWith('.md') && !f.endsWith(CLOUD_COPY)).sort()) {
|
|
22
|
+
const { data, body } = parseFrontmatter(readFileSync(join(dir, file), 'utf-8'));
|
|
23
|
+
pages.push({
|
|
24
|
+
path: `${WIKI_DIR}/${folder}/${file}`,
|
|
25
|
+
kind,
|
|
26
|
+
title: String(data.title ?? file.replace(/\.md$/, '')),
|
|
27
|
+
scope: data.scope ?? [],
|
|
28
|
+
freq: Number(data.freq ?? 1),
|
|
29
|
+
severity: data.severity === 'critical' ? 'critical' : 'warning',
|
|
30
|
+
cards: data.cards ?? [],
|
|
31
|
+
updated: String(data.updated ?? ''),
|
|
32
|
+
...(data.promoted ? { promoted: true } : {}),
|
|
33
|
+
body: body.trim(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return pages;
|
|
38
|
+
}
|
|
39
|
+
/** Pages that changed here and in the cloud: the local file and the cloud version written beside it, waiting for a merge. */
|
|
40
|
+
export function cloudCopies(projectDir) {
|
|
41
|
+
return [...Object.values(KINDS), 'competitors'].flatMap(folder => {
|
|
42
|
+
const dir = join(projectDir, WIKI_DIR, folder);
|
|
43
|
+
if (!existsSync(dir))
|
|
44
|
+
return [];
|
|
45
|
+
return readdirSync(dir).filter(f => f.endsWith(CLOUD_COPY)).sort().map(f => ({ path: `${WIKI_DIR}/${folder}/${f.slice(0, -CLOUD_COPY.length)}.md`, cloud: `${WIKI_DIR}/${folder}/${f}` }));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function writePage(projectDir, page) {
|
|
49
|
+
const { path, kind: _kind, body, ...frontmatter } = page;
|
|
50
|
+
mkdirSync(dirname(join(projectDir, path)), { recursive: true });
|
|
51
|
+
writeFileSync(join(projectDir, path), `---\n${stringify(frontmatter)}---\n\n${body}\n`);
|
|
52
|
+
}
|
|
53
|
+
function writeIndex(projectDir) {
|
|
54
|
+
const pages = listPages(projectDir);
|
|
55
|
+
const lines = ['# Wiki', '', 'Generated by `codeloop wiki capture`. Run `codeloop wiki inject --files <paths>` to get the pages that apply to a change.', ''];
|
|
56
|
+
for (const [kind, folder] of Object.entries(KINDS)) {
|
|
57
|
+
lines.push(`## ${folder[0].toUpperCase()}${folder.slice(1)}`, '');
|
|
58
|
+
const mine = pages.filter(p => p.kind === kind);
|
|
59
|
+
if (mine.length === 0)
|
|
60
|
+
lines.push('None yet.');
|
|
61
|
+
for (const p of mine)
|
|
62
|
+
lines.push(`- [${p.title}](${relative(WIKI_DIR, p.path)}): freq ${p.freq}, ${p.severity}, scope ${p.scope.join(', ') || 'none'}`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
mkdirSync(join(projectDir, WIKI_DIR), { recursive: true });
|
|
66
|
+
writeFileSync(join(projectDir, WIKI_DIR, 'INDEX.md'), lines.join('\n'));
|
|
67
|
+
}
|
|
68
|
+
export function thresholds(projectDir) {
|
|
69
|
+
const config = loadConfig(projectDir);
|
|
70
|
+
return { critical: config.codeloop?.critical_frequency ?? 3, promote: config.codeloop?.promote_frequency ?? 10 };
|
|
71
|
+
}
|
|
72
|
+
/** A title that already exists is the same lesson met again: its frequency goes up instead of a second page. */
|
|
73
|
+
export function capture(projectDir, input, now = clock()) {
|
|
74
|
+
const kind = input.kind ?? 'gotcha';
|
|
75
|
+
if (!KINDS[kind])
|
|
76
|
+
throw new RefusalError(`unknown kind "${kind}" (gotcha, decision or concept)`);
|
|
77
|
+
if (!slugify(input.title))
|
|
78
|
+
throw new RefusalError(`wiki title "${input.title}" has no letters or digits to name its file with`);
|
|
79
|
+
// A second capture of the same title reads the page, raises its frequency and writes it back.
|
|
80
|
+
return withLock(join(projectDir, WIKI_DIR, 'INDEX.md'), () => captureLocked(projectDir, input, kind, now));
|
|
81
|
+
}
|
|
82
|
+
function captureLocked(projectDir, input, kind, now) {
|
|
83
|
+
const today = now.toISOString().slice(0, 10);
|
|
84
|
+
const existing = listPages(projectDir).find(p => p.title.toLowerCase() === input.title.toLowerCase());
|
|
85
|
+
const page = existing
|
|
86
|
+
? {
|
|
87
|
+
...existing,
|
|
88
|
+
freq: existing.freq + 1,
|
|
89
|
+
scope: [...new Set([...existing.scope, ...input.scope])],
|
|
90
|
+
cards: [...new Set([...existing.cards, ...(input.card ? [input.card] : [])])],
|
|
91
|
+
updated: today,
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
path: `${WIKI_DIR}/${KINDS[kind]}/${slugify(input.title)}.md`,
|
|
95
|
+
kind,
|
|
96
|
+
title: input.title,
|
|
97
|
+
scope: input.scope,
|
|
98
|
+
freq: 1,
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
cards: input.card ? [input.card] : [],
|
|
101
|
+
updated: today,
|
|
102
|
+
body: input.body.trim(),
|
|
103
|
+
};
|
|
104
|
+
if (page.freq >= thresholds(projectDir).critical)
|
|
105
|
+
page.severity = 'critical';
|
|
106
|
+
writePage(projectDir, page);
|
|
107
|
+
writeIndex(projectDir);
|
|
108
|
+
writeCloudWikiPage(projectDir, page.path);
|
|
109
|
+
return page;
|
|
110
|
+
}
|
|
111
|
+
export function inject(projectDir, files) {
|
|
112
|
+
const paths = files.map(f => relative(projectDir, resolve(projectDir, f)));
|
|
113
|
+
return listPages(projectDir).filter(p => p.scope.some(glob => paths.some(path => globMatches(glob, path))));
|
|
114
|
+
}
|
|
115
|
+
/** Applies the frequency thresholds: critical at `critical_frequency`, copied into rules.md once at `promote_frequency`. */
|
|
116
|
+
export function learn(projectDir) {
|
|
117
|
+
const { critical, promote } = thresholds(projectDir);
|
|
118
|
+
const result = { critical: [], promoted: [] };
|
|
119
|
+
for (const page of listPages(projectDir)) {
|
|
120
|
+
let changed = false;
|
|
121
|
+
if (page.freq >= critical && page.severity !== 'critical') {
|
|
122
|
+
page.severity = 'critical';
|
|
123
|
+
result.critical.push(page.title);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
if (page.freq >= promote && !page.promoted) {
|
|
127
|
+
const rules = join(projectDir, '.codeloop/rules.md');
|
|
128
|
+
const current = existsSync(rules) ? readFileSync(rules, 'utf-8') : '# Rules\n';
|
|
129
|
+
writeFileSync(rules, `${current.trimEnd()}\n\n## ${page.title}\n\nPromoted from ${page.path} at freq ${page.freq}. Scope: ${page.scope.join(', ') || 'all'}.\n\n${page.body}\n`);
|
|
130
|
+
page.promoted = true;
|
|
131
|
+
result.promoted.push(page.title);
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
if (changed)
|
|
135
|
+
writePage(projectDir, page);
|
|
136
|
+
}
|
|
137
|
+
if (result.critical.length || result.promoted.length)
|
|
138
|
+
writeIndex(projectDir);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
export function lintWiki(projectDir, now = clock()) {
|
|
142
|
+
const pages = listPages(projectDir);
|
|
143
|
+
const broken = [];
|
|
144
|
+
const files = [...pages.map(p => ({ path: p.path, text: p.body })), { path: `${WIKI_DIR}/INDEX.md`, text: existsSync(join(projectDir, WIKI_DIR, 'INDEX.md')) ? readFileSync(join(projectDir, WIKI_DIR, 'INDEX.md'), 'utf-8') : '' }];
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
for (const m of file.text.matchAll(/\[[^\]]*\]\(([^)#\s]+)(?:#[^)]*)?\)/g)) {
|
|
147
|
+
if (/^[a-z]+:/i.test(m[1]))
|
|
148
|
+
continue;
|
|
149
|
+
if (!existsSync(resolve(projectDir, dirname(file.path), m[1])))
|
|
150
|
+
broken.push(`${file.path}: link to ${m[1]} does not resolve`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const cutoff = new Date(now.getTime() - STALE_DAYS * 86_400_000).toISOString().slice(0, 10);
|
|
154
|
+
const stale = pages.filter(p => p.updated && p.updated < cutoff).map(p => `${p.path}: last updated ${p.updated}, over ${STALE_DAYS} days ago`);
|
|
155
|
+
const titles = pages.map(p => p.title.toLowerCase());
|
|
156
|
+
const duplicates = pages.filter((p, i) => titles.indexOf(p.title.toLowerCase()) !== i).map(p => `${p.path}: duplicate title "${p.title}"`);
|
|
157
|
+
return { broken, stale, duplicates };
|
|
158
|
+
}
|
|
159
|
+
/** Critical pages whose scope matches the files and that were not acknowledged by title. */
|
|
160
|
+
export function blockingGotchas(projectDir, files, acked) {
|
|
161
|
+
const ack = new Set(acked.map(t => t.toLowerCase()));
|
|
162
|
+
return inject(projectDir, files).filter(p => p.severity === 'critical' && !ack.has(p.title.toLowerCase()));
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=wiki.js.map
|