@on-par/factory-core 0.9.27-next.3 → 0.9.27-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/readiness/decompose-coverage.d.ts.map +1 -1
- package/dist/readiness/decompose-coverage.js +1 -2
- package/dist/readiness/decompose-coverage.js.map +1 -1
- package/dist/readiness/decompose.d.ts +1 -6
- package/dist/readiness/decompose.d.ts.map +1 -1
- package/dist/readiness/decompose.js +3 -13
- package/dist/readiness/decompose.js.map +1 -1
- package/dist/readiness/size.d.ts.map +1 -1
- package/dist/readiness/size.js +2 -5
- package/dist/readiness/size.js.map +1 -1
- package/dist/router/stub.js +1 -1
- package/dist/router/stub.js.map +1 -1
- package/dist/run/ports.d.ts +1 -1
- package/dist/run/ports.d.ts.map +1 -1
- package/dist/{fakes → testing/sim}/model.d.ts +3 -3
- package/dist/testing/sim/model.d.ts.map +1 -0
- package/dist/{fakes → testing/sim}/model.js +3 -3
- package/dist/testing/sim/model.js.map +1 -0
- package/dist/testing/sim/workspace.d.ts.map +1 -0
- package/dist/{fakes → testing/sim}/workspace.js +10 -9
- package/dist/testing/sim/workspace.js.map +1 -0
- package/dist/testing.d.ts +4 -4
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +5 -5
- package/dist/discovery/author.d.ts +0 -54
- package/dist/discovery/author.d.ts.map +0 -1
- package/dist/discovery/author.js +0 -178
- package/dist/discovery/author.js.map +0 -1
- package/dist/discovery/gherkin-scenario.d.ts +0 -4
- package/dist/discovery/gherkin-scenario.d.ts.map +0 -1
- package/dist/discovery/gherkin-scenario.js +0 -2
- package/dist/discovery/gherkin-scenario.js.map +0 -1
- package/dist/discovery/index.d.ts +0 -40
- package/dist/discovery/index.d.ts.map +0 -1
- package/dist/discovery/index.js +0 -342
- package/dist/discovery/index.js.map +0 -1
- package/dist/discovery/promote.d.ts +0 -82
- package/dist/discovery/promote.d.ts.map +0 -1
- package/dist/discovery/promote.js +0 -307
- package/dist/discovery/promote.js.map +0 -1
- package/dist/fakes/model.d.ts.map +0 -1
- package/dist/fakes/model.js.map +0 -1
- package/dist/fakes/workspace.d.ts.map +0 -1
- package/dist/fakes/workspace.js.map +0 -1
- /package/dist/{fakes → testing/sim}/workspace.d.ts +0 -0
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { CommandResult } from '../utils/command-runner.js';
|
|
2
|
-
import type { DiscoveryCandidate } from './index.js';
|
|
3
|
-
export declare const DISCOVERY_LABEL = "discovery";
|
|
4
|
-
export declare const EXPLORING_LABEL = "status:exploring";
|
|
5
|
-
export declare const DEFAULT_OWNER_QUESTIONS: readonly string[];
|
|
6
|
-
export interface AuthorDraftEpicOptions {
|
|
7
|
-
repoDir: string;
|
|
8
|
-
candidate: DiscoveryCandidate;
|
|
9
|
-
/** Optional richer content; when omitted, each is seeded deterministically from the candidate/signals. */
|
|
10
|
-
userProblem?: string;
|
|
11
|
-
whyNow?: string;
|
|
12
|
-
wedge?: string;
|
|
13
|
-
/** Owner questions; when omitted, DEFAULT_OWNER_QUESTIONS is used. */
|
|
14
|
-
questions?: string[];
|
|
15
|
-
}
|
|
16
|
-
export interface AuthorDraftEpicDeps {
|
|
17
|
-
now?: () => Date;
|
|
18
|
-
run?: (argv: readonly string[], opts: {
|
|
19
|
-
cwd: string;
|
|
20
|
-
}) => Promise<Pick<CommandResult, 'stdout' | 'ok'>>;
|
|
21
|
-
}
|
|
22
|
-
export type AuthorDraftEpicResult = {
|
|
23
|
-
created: true;
|
|
24
|
-
issueNumber: number;
|
|
25
|
-
issueUrl: string;
|
|
26
|
-
labels: string[];
|
|
27
|
-
commentPosted: boolean;
|
|
28
|
-
} | {
|
|
29
|
-
created: false;
|
|
30
|
-
reason: 'duplicate';
|
|
31
|
-
duplicateOf: number;
|
|
32
|
-
} | {
|
|
33
|
-
created: false;
|
|
34
|
-
reason: 'list-failed';
|
|
35
|
-
detail: string;
|
|
36
|
-
} | {
|
|
37
|
-
created: false;
|
|
38
|
-
reason: 'create-failed';
|
|
39
|
-
detail: string;
|
|
40
|
-
};
|
|
41
|
-
export declare function ideaSlug(hypothesis: string): string;
|
|
42
|
-
export declare function ideaMarker(hypothesis: string): string;
|
|
43
|
-
export declare function draftEpicTitle(hypothesis: string): string;
|
|
44
|
-
export declare function normalizeTitle(title: string): string;
|
|
45
|
-
export declare function findDuplicate(existing: ReadonlyArray<{
|
|
46
|
-
number: number;
|
|
47
|
-
title: string;
|
|
48
|
-
body: string;
|
|
49
|
-
}>, candidate: DiscoveryCandidate, title: string): number | null;
|
|
50
|
-
export declare function parseIssueNumber(url: string): number | null;
|
|
51
|
-
export declare function renderDraftEpicBody(options: AuthorDraftEpicOptions): string;
|
|
52
|
-
export declare function renderOwnerComment(questions: readonly string[]): string;
|
|
53
|
-
export declare function authorDraftEpic(options: AuthorDraftEpicOptions, deps?: AuthorDraftEpicDeps): Promise<AuthorDraftEpicResult>;
|
|
54
|
-
//# sourceMappingURL=author.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"author.d.ts","sourceRoot":"","sources":["../../src/discovery/author.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAQlD,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAIpD,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,kBAAkB,CAAC;IAC9B,0GAA0G;IAC1G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC;CACzG;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,GAClG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAIhE,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACxE,SAAS,EAAE,kBAAkB,EAC7B,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAQf;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAmC3E;AAID,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CASvE;AAID,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,EAC/B,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CA8FhC"}
|
package/dist/discovery/author.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { runCommand } from '../utils/command-runner.js';
|
|
2
|
-
export const DISCOVERY_LABEL = 'discovery';
|
|
3
|
-
export const EXPLORING_LABEL = 'status:exploring';
|
|
4
|
-
const DISCOVERY_LABEL_COLOR = '5319e7'; // purple
|
|
5
|
-
const EXPLORING_LABEL_COLOR = 'fbca04'; // yellow
|
|
6
|
-
const DISCOVERY_LABEL_DESC = 'Discovery-phase idea Epic under exploration';
|
|
7
|
-
const EXPLORING_LABEL_DESC = 'Idea is being explored; owner review pending';
|
|
8
|
-
const TITLE_MAX_LEN = 120;
|
|
9
|
-
export const DEFAULT_OWNER_QUESTIONS = [
|
|
10
|
-
'Is this problem worth solving now?',
|
|
11
|
-
'Does the proposed wedge/scope match how you would cut the first slice?',
|
|
12
|
-
'What signal are we missing that would raise or lower the priority?',
|
|
13
|
-
];
|
|
14
|
-
// ---------- Pure helpers ----------
|
|
15
|
-
export function ideaSlug(hypothesis) {
|
|
16
|
-
return hypothesis
|
|
17
|
-
.toLowerCase()
|
|
18
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
-
.replace(/-+/g, '-')
|
|
20
|
-
.replace(/^-|-$/g, '')
|
|
21
|
-
.slice(0, 80)
|
|
22
|
-
.replace(/-+$/g, '');
|
|
23
|
-
}
|
|
24
|
-
export function ideaMarker(hypothesis) {
|
|
25
|
-
return `<!-- factory:idea key="${ideaSlug(hypothesis)}" -->`;
|
|
26
|
-
}
|
|
27
|
-
export function draftEpicTitle(hypothesis) {
|
|
28
|
-
const truncated = hypothesis.length > TITLE_MAX_LEN ? `${hypothesis.slice(0, TITLE_MAX_LEN).trimEnd()}…` : hypothesis;
|
|
29
|
-
return `Draft Epic: ${truncated}`;
|
|
30
|
-
}
|
|
31
|
-
export function normalizeTitle(title) {
|
|
32
|
-
return title
|
|
33
|
-
.toLowerCase()
|
|
34
|
-
.replace(/^(draft epic:|epic:)\s*/, '')
|
|
35
|
-
.replace(/\s+/g, ' ')
|
|
36
|
-
.trim()
|
|
37
|
-
.replace(/[.,;:!?]+$/, '');
|
|
38
|
-
}
|
|
39
|
-
export function findDuplicate(existing, candidate, title) {
|
|
40
|
-
const marker = ideaMarker(candidate.hypothesis);
|
|
41
|
-
const normalizedTitle = normalizeTitle(title);
|
|
42
|
-
for (const item of existing) {
|
|
43
|
-
if (item.body.includes(marker))
|
|
44
|
-
return item.number;
|
|
45
|
-
if (normalizeTitle(item.title) === normalizedTitle)
|
|
46
|
-
return item.number;
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
export function parseIssueNumber(url) {
|
|
51
|
-
const match = /(\d+)\s*$/.exec(url.trim());
|
|
52
|
-
return match ? Number(match[1]) : null;
|
|
53
|
-
}
|
|
54
|
-
export function renderDraftEpicBody(options) {
|
|
55
|
-
const { candidate } = options;
|
|
56
|
-
const questions = options.questions ?? DEFAULT_OWNER_QUESTIONS;
|
|
57
|
-
const primarySignal = candidate.signals[0];
|
|
58
|
-
const userProblem = options.userProblem ?? `Signal suggests: ${primarySignal.detail} — refine with the real user problem.`;
|
|
59
|
-
const whyNow = options.whyNow ?? `Ranked score ${candidate.score}; surfaced from ${candidate.signals.length} signal(s).`;
|
|
60
|
-
const wedge = options.wedge ?? 'To be scoped — smallest slice that tests the hypothesis.';
|
|
61
|
-
const questionsList = questions.map((q) => `- ${q}`).join('\n');
|
|
62
|
-
const signalsList = candidate.signals.map((s) => `- \`${s.reference}\` — ${s.detail}`).join('\n');
|
|
63
|
-
return [
|
|
64
|
-
'## Hypothesis',
|
|
65
|
-
candidate.hypothesis,
|
|
66
|
-
'',
|
|
67
|
-
'## User problem',
|
|
68
|
-
userProblem,
|
|
69
|
-
'',
|
|
70
|
-
'## Why now',
|
|
71
|
-
whyNow,
|
|
72
|
-
'',
|
|
73
|
-
'## Rough wedge / scope',
|
|
74
|
-
wedge,
|
|
75
|
-
'',
|
|
76
|
-
'## Open Questions',
|
|
77
|
-
questionsList,
|
|
78
|
-
'',
|
|
79
|
-
'## Signals',
|
|
80
|
-
signalsList,
|
|
81
|
-
'',
|
|
82
|
-
ideaMarker(candidate.hypothesis),
|
|
83
|
-
].join('\n');
|
|
84
|
-
}
|
|
85
|
-
// GitHub/gh has no per-comment pin API; this comment is the honest interpretation of "pinned" —
|
|
86
|
-
// a clearly-marked, structured owner-review comment instead of a true pin.
|
|
87
|
-
export function renderOwnerComment(questions) {
|
|
88
|
-
const numbered = questions.map((q, i) => `${i + 1}. ${q}`).join('\n');
|
|
89
|
-
return [
|
|
90
|
-
'📌 **Owner review needed**',
|
|
91
|
-
'',
|
|
92
|
-
numbered,
|
|
93
|
-
'',
|
|
94
|
-
'Answer in the comments. Set `validated` to proceed, `needs-work` to revise, or `wontfix` to drop.',
|
|
95
|
-
].join('\n');
|
|
96
|
-
}
|
|
97
|
-
// ---------- Main entry point ----------
|
|
98
|
-
export async function authorDraftEpic(options, deps = {}) {
|
|
99
|
-
const run = deps.run ?? ((argv, o) => runCommand(argv, { cwd: o.cwd }));
|
|
100
|
-
const { repoDir, candidate } = options;
|
|
101
|
-
const questions = options.questions ?? DEFAULT_OWNER_QUESTIONS;
|
|
102
|
-
const title = draftEpicTitle(candidate.hypothesis);
|
|
103
|
-
const listResult = await run([
|
|
104
|
-
'gh',
|
|
105
|
-
'issue',
|
|
106
|
-
'list',
|
|
107
|
-
'--label',
|
|
108
|
-
DISCOVERY_LABEL,
|
|
109
|
-
'--state',
|
|
110
|
-
'all',
|
|
111
|
-
'--limit',
|
|
112
|
-
'100',
|
|
113
|
-
'--json',
|
|
114
|
-
'number,title,body',
|
|
115
|
-
], { cwd: repoDir });
|
|
116
|
-
if (!listResult.ok) {
|
|
117
|
-
return { created: false, reason: 'list-failed', detail: listResult.stdout || 'gh issue list failed' };
|
|
118
|
-
}
|
|
119
|
-
let existing;
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(listResult.stdout);
|
|
122
|
-
if (!Array.isArray(parsed)) {
|
|
123
|
-
return { created: false, reason: 'list-failed', detail: 'unparseable gh issue list output' };
|
|
124
|
-
}
|
|
125
|
-
existing = parsed;
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return { created: false, reason: 'list-failed', detail: 'unparseable gh issue list output' };
|
|
129
|
-
}
|
|
130
|
-
const dup = findDuplicate(existing, candidate, title);
|
|
131
|
-
if (dup !== null) {
|
|
132
|
-
return { created: false, reason: 'duplicate', duplicateOf: dup };
|
|
133
|
-
}
|
|
134
|
-
const labelListResult = await run(['gh', 'label', 'list', '--limit', '200', '--json', 'name'], { cwd: repoDir });
|
|
135
|
-
let existingLabels = [];
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(labelListResult.stdout);
|
|
138
|
-
if (Array.isArray(parsed)) {
|
|
139
|
-
existingLabels = parsed
|
|
140
|
-
.map((item) => item.name)
|
|
141
|
-
.filter((name) => typeof name === 'string');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
existingLabels = [];
|
|
146
|
-
}
|
|
147
|
-
const labelSpecs = [
|
|
148
|
-
{ name: DISCOVERY_LABEL, color: DISCOVERY_LABEL_COLOR, description: DISCOVERY_LABEL_DESC },
|
|
149
|
-
{ name: EXPLORING_LABEL, color: EXPLORING_LABEL_COLOR, description: EXPLORING_LABEL_DESC },
|
|
150
|
-
];
|
|
151
|
-
for (const spec of labelSpecs) {
|
|
152
|
-
if (existingLabels.includes(spec.name))
|
|
153
|
-
continue;
|
|
154
|
-
await run(['gh', 'label', 'create', spec.name, '--color', spec.color, '--description', spec.description], {
|
|
155
|
-
cwd: repoDir,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
const body = renderDraftEpicBody(options);
|
|
159
|
-
const createResult = await run(['gh', 'issue', 'create', '--title', title, '--body', body, '--label', DISCOVERY_LABEL, '--label', EXPLORING_LABEL], { cwd: repoDir });
|
|
160
|
-
if (!createResult.ok) {
|
|
161
|
-
return { created: false, reason: 'create-failed', detail: createResult.stdout || 'gh issue create failed' };
|
|
162
|
-
}
|
|
163
|
-
const issueNumber = parseIssueNumber(createResult.stdout);
|
|
164
|
-
if (issueNumber === null) {
|
|
165
|
-
return { created: false, reason: 'create-failed', detail: createResult.stdout || 'unparseable issue URL' };
|
|
166
|
-
}
|
|
167
|
-
const commentResult = await run(['gh', 'issue', 'comment', String(issueNumber), '--body', renderOwnerComment(questions)], {
|
|
168
|
-
cwd: repoDir,
|
|
169
|
-
});
|
|
170
|
-
return {
|
|
171
|
-
created: true,
|
|
172
|
-
issueNumber,
|
|
173
|
-
issueUrl: createResult.stdout.trim(),
|
|
174
|
-
labels: [DISCOVERY_LABEL, EXPLORING_LABEL],
|
|
175
|
-
commentPosted: commentResult.ok,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
//# sourceMappingURL=author.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"author.js","sourceRoot":"","sources":["../../src/discovery/author.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAGxD,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAClD,MAAM,qBAAqB,GAAG,QAAQ,CAAC,CAAC,SAAS;AACjD,MAAM,qBAAqB,GAAG,QAAQ,CAAC,CAAC,SAAS;AACjD,MAAM,oBAAoB,GAAG,6CAA6C,CAAC;AAC3E,MAAM,oBAAoB,GAAG,8CAA8C,CAAC;AAE5E,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,MAAM,CAAC,MAAM,uBAAuB,GAAsB;IACxD,oCAAoC;IACpC,wEAAwE;IACxE,oEAAoE;CACrE,CAAC;AAwBF,qCAAqC;AAErC,MAAM,UAAU,QAAQ,CAAC,UAAkB;IACzC,OAAO,UAAU;SACd,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,UAAkB;IAC3C,OAAO,0BAA0B,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,UAAkB;IAC/C,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC;IACtH,OAAO,eAAe,SAAS,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC;SACtC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,QAAwE,EACxE,SAA6B,EAC7B,KAAa;IAEb,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QACnD,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,eAAe;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IACzE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3C,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAA+B;IACjE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,uBAAuB,CAAC;IAC/D,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAE3C,MAAM,WAAW,GACf,OAAO,CAAC,WAAW,IAAI,oBAAoB,aAAa,CAAC,MAAM,uCAAuC,CAAC;IACzG,MAAM,MAAM,GACV,OAAO,CAAC,MAAM,IAAI,gBAAgB,SAAS,CAAC,KAAK,mBAAmB,SAAS,CAAC,OAAO,CAAC,MAAM,aAAa,CAAC;IAC5G,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,0DAA0D,CAAC;IAE1F,MAAM,aAAa,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAElG,OAAO;QACL,eAAe;QACf,SAAS,CAAC,UAAU;QACpB,EAAE;QACF,iBAAiB;QACjB,WAAW;QACX,EAAE;QACF,YAAY;QACZ,MAAM;QACN,EAAE;QACF,wBAAwB;QACxB,KAAK;QACL,EAAE;QACF,mBAAmB;QACnB,aAAa;QACb,EAAE;QACF,YAAY;QACZ,WAAW;QACX,EAAE;QACF,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC;KACjC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,gGAAgG;AAChG,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,SAA4B;IAC7D,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,OAAO;QACL,4BAA4B;QAC5B,EAAE;QACF,QAAQ;QACR,EAAE;QACF,mGAAmG;KACpG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,yCAAyC;AAEzC,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B,EAC/B,IAAI,GAAwB,EAAE;IAE9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAuB,EAAE,CAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC5G,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,uBAAuB,CAAC;IAE/D,MAAM,KAAK,GAAG,cAAc,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAEnD,MAAM,UAAU,GAAG,MAAM,GAAG,CAC1B;QACE,IAAI;QACJ,OAAO;QACP,MAAM;QACN,SAAS;QACT,eAAe;QACf,SAAS;QACT,KAAK;QACL,SAAS;QACT,KAAK;QACL,QAAQ;QACR,mBAAmB;KACpB,EACD,EAAE,GAAG,EAAE,OAAO,EAAE,CACjB,CAAC;IACF,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,IAAI,sBAAsB,EAAE,CAAC;IACxG,CAAC;IACD,IAAI,QAAgE,CAAC;IACrE,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;QAC/F,CAAC;QACD,QAAQ,GAAG,MAAgE,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;IAC/F,CAAC;IAED,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IACtD,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;IACnE,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IACjH,IAAI,cAAc,GAAa,EAAE,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,cAAc,GAAI,MAAoC;iBACnD,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;iBACxB,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,cAAc,GAAG,EAAE,CAAC;IACtB,CAAC;IAED,MAAM,UAAU,GAAgE;QAC9E,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,oBAAoB,EAAE;QAC1F,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,oBAAoB,EAAE;KAC3F,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACjD,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE;YACxG,GAAG,EAAE,OAAO;SACb,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,MAAM,GAAG,CAC5B,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe,CAAC,EACnH,EAAE,GAAG,EAAE,OAAO,EAAE,CACjB,CAAC;IACF,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;QACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,IAAI,wBAAwB,EAAE,CAAC;IAC9G,CAAC;IAED,MAAM,WAAW,GAAG,gBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAC1D,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAAC;IAC7G,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,GAAG,CAC7B,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC,EACxF;QACE,GAAG,EAAE,OAAO;KACb,CACF,CAAC;IAEF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,WAAW;QACX,QAAQ,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE;QACpC,MAAM,EAAE,CAAC,eAAe,EAAE,eAAe,CAAC;QAC1C,aAAa,EAAE,aAAa,CAAC,EAAE;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gherkin-scenario.d.ts","sourceRoot":"","sources":["../../src/discovery/gherkin-scenario.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,oGAAoG;AACpG,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gherkin-scenario.js","sourceRoot":"","sources":["../../src/discovery/gherkin-scenario.ts"],"names":[],"mappings":""}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { CommandResult } from '../utils/command-runner.js';
|
|
2
|
-
export type DiscoverySignalSource = 'constitution' | 'roadmap' | 'code-todo' | 'feedback' | 'bug-theme';
|
|
3
|
-
export interface DiscoverySignal {
|
|
4
|
-
source: DiscoverySignalSource;
|
|
5
|
-
/** Where the signal came from, e.g. "packages/x/y.ts:42", "ROADMAP.md", ".factory/fingerprints.json" */
|
|
6
|
-
reference: string;
|
|
7
|
-
/** The signal text, trimmed to a single line. */
|
|
8
|
-
detail: string;
|
|
9
|
-
/** Recurrence count; only meaningful for 'bug-theme'. Defaults to 1. */
|
|
10
|
-
count?: number;
|
|
11
|
-
}
|
|
12
|
-
export interface DiscoveryCandidate {
|
|
13
|
-
/** One-line hypothesis. */
|
|
14
|
-
hypothesis: string;
|
|
15
|
-
/** Ranking score; higher = more compelling. */
|
|
16
|
-
score: number;
|
|
17
|
-
/** The concrete signals that motivated this idea. ALWAYS length >= 1. */
|
|
18
|
-
signals: DiscoverySignal[];
|
|
19
|
-
}
|
|
20
|
-
export interface DiscoveryScanResult {
|
|
21
|
-
scannedAt: string;
|
|
22
|
-
repoDir: string;
|
|
23
|
-
signalsCollected: number;
|
|
24
|
-
candidates: DiscoveryCandidate[];
|
|
25
|
-
}
|
|
26
|
-
export interface DiscoveryScanOptions {
|
|
27
|
-
repoDir: string;
|
|
28
|
-
/** Max candidates per cycle. Falls back to DEFAULT_MAX_CANDIDATES (5) when omitted. */
|
|
29
|
-
maxCandidates?: number;
|
|
30
|
-
}
|
|
31
|
-
export interface DiscoveryScanDeps {
|
|
32
|
-
now?: () => Date;
|
|
33
|
-
/** Runs a read-only external command (only used for `gh issue list`). Defaults to runCommand. */
|
|
34
|
-
run?: (argv: readonly string[], opts: {
|
|
35
|
-
cwd: string;
|
|
36
|
-
}) => Promise<Pick<CommandResult, 'stdout' | 'ok'>>;
|
|
37
|
-
}
|
|
38
|
-
export declare const DEFAULT_MAX_CANDIDATES = 5;
|
|
39
|
-
export declare function runDiscoveryScan(options: DiscoveryScanOptions, deps?: DiscoveryScanDeps): Promise<DiscoveryScanResult>;
|
|
40
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/discovery/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,MAAM,MAAM,qBAAqB,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,CAAC;AAExG,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,qBAAqB,CAAC;IAC9B,wGAAwG;IACxG,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,kBAAkB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,iGAAiG;IACjG,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC;CACzG;AAED,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAiTxC,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,EAC7B,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAyC9B"}
|
package/dist/discovery/index.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
// src/discovery/index.ts — Read-only discovery scan: rank candidate ideas from product signals
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
-
import { extname, join, relative } from 'node:path';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { ConstitutionLoader } from '../constitutions/index.js';
|
|
6
|
-
import { runCommand } from '../utils/command-runner.js';
|
|
7
|
-
export const DEFAULT_MAX_CANDIDATES = 5;
|
|
8
|
-
const SOURCE_WEIGHT = {
|
|
9
|
-
'bug-theme': 5,
|
|
10
|
-
roadmap: 4,
|
|
11
|
-
feedback: 3,
|
|
12
|
-
'code-todo': 2,
|
|
13
|
-
constitution: 2,
|
|
14
|
-
};
|
|
15
|
-
// Source priority for deterministic tie-breaking (higher first):
|
|
16
|
-
const SOURCE_PRIORITY = {
|
|
17
|
-
'bug-theme': 4,
|
|
18
|
-
roadmap: 3,
|
|
19
|
-
feedback: 2,
|
|
20
|
-
'code-todo': 1,
|
|
21
|
-
constitution: 0,
|
|
22
|
-
};
|
|
23
|
-
const MAX_SCANNED_FILES = 2000; // guard against huge trees
|
|
24
|
-
const MAX_FILE_BYTES = 512 * 1024; // skip large files
|
|
25
|
-
const OPEN_ISSUES_TIMEOUT_MS = 2_000;
|
|
26
|
-
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.factory', 'coverage', '.turbo']);
|
|
27
|
-
const TODO_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.md', '.mjs', '.cjs']);
|
|
28
|
-
const STOPWORDS = new Set([
|
|
29
|
-
'the',
|
|
30
|
-
'and',
|
|
31
|
-
'for',
|
|
32
|
-
'with',
|
|
33
|
-
'this',
|
|
34
|
-
'that',
|
|
35
|
-
'from',
|
|
36
|
-
'into',
|
|
37
|
-
'your',
|
|
38
|
-
'our',
|
|
39
|
-
'are',
|
|
40
|
-
'was',
|
|
41
|
-
'will',
|
|
42
|
-
'when',
|
|
43
|
-
'then',
|
|
44
|
-
'than',
|
|
45
|
-
'have',
|
|
46
|
-
'has',
|
|
47
|
-
'not',
|
|
48
|
-
'but',
|
|
49
|
-
'all',
|
|
50
|
-
'any',
|
|
51
|
-
'can',
|
|
52
|
-
'use',
|
|
53
|
-
'add',
|
|
54
|
-
'fix',
|
|
55
|
-
'via',
|
|
56
|
-
]);
|
|
57
|
-
const FingerprintThemeSchema = z.object({ key: z.string(), label: z.string(), count: z.number() });
|
|
58
|
-
const FingerprintsSchema = z.object({ themes: z.array(FingerprintThemeSchema).default([]) });
|
|
59
|
-
// ---------- Token helpers ----------
|
|
60
|
-
function tokenize(text) {
|
|
61
|
-
return text
|
|
62
|
-
.toLowerCase()
|
|
63
|
-
.split(/[^a-z0-9]+/)
|
|
64
|
-
.filter(Boolean);
|
|
65
|
-
}
|
|
66
|
-
function significantTokens(text) {
|
|
67
|
-
return tokenize(text).filter((token) => token.length >= 4 && !STOPWORDS.has(token));
|
|
68
|
-
}
|
|
69
|
-
function sharesSignificantToken(detail, titles) {
|
|
70
|
-
const detailTokens = new Set(significantTokens(detail));
|
|
71
|
-
if (detailTokens.size === 0)
|
|
72
|
-
return false;
|
|
73
|
-
for (const title of titles) {
|
|
74
|
-
for (const token of significantTokens(title)) {
|
|
75
|
-
if (detailTokens.has(token))
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
function northStarKeywords(signals) {
|
|
82
|
-
const keywords = new Set();
|
|
83
|
-
for (const signal of signals) {
|
|
84
|
-
for (const token of tokenize(signal.detail)) {
|
|
85
|
-
if (token.length >= 4 && !STOPWORDS.has(token))
|
|
86
|
-
keywords.add(token);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return keywords;
|
|
90
|
-
}
|
|
91
|
-
// ---------- Reader helpers ----------
|
|
92
|
-
function collectConstitutionSignals(repoDir) {
|
|
93
|
-
let constitution;
|
|
94
|
-
try {
|
|
95
|
-
constitution = new ConstitutionLoader().loadFromRepo(repoDir);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
if (!constitution)
|
|
101
|
-
return [];
|
|
102
|
-
const reference = constitution.source === 'repo' ? 'CLAUDE.md' : constitution.path;
|
|
103
|
-
const signals = [];
|
|
104
|
-
const headingRe = /^#{1,6}\s+(.+)$/gm;
|
|
105
|
-
let match;
|
|
106
|
-
while ((match = headingRe.exec(constitution.body)) !== null) {
|
|
107
|
-
signals.push({ source: 'constitution', reference, detail: match[1].trim() });
|
|
108
|
-
}
|
|
109
|
-
return signals;
|
|
110
|
-
}
|
|
111
|
-
function readFirstExisting(repoDir, relPaths) {
|
|
112
|
-
for (const relPath of relPaths) {
|
|
113
|
-
try {
|
|
114
|
-
const content = readFileSync(join(repoDir, relPath), 'utf-8');
|
|
115
|
-
return { relPath, content };
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function parseListItems(content) {
|
|
124
|
-
const items = [];
|
|
125
|
-
for (const line of content.split('\n')) {
|
|
126
|
-
const bulletMatch = /^\s*[-*]\s+(.+)$/.exec(line);
|
|
127
|
-
const numberedMatch = /^\s*\d+\.\s+(.+)$/.exec(line);
|
|
128
|
-
const raw = bulletMatch?.[1] ?? numberedMatch?.[1];
|
|
129
|
-
if (!raw)
|
|
130
|
-
continue;
|
|
131
|
-
const stripped = raw.replace(/^\[[ xX]\]\s*/, '').trim();
|
|
132
|
-
if (stripped)
|
|
133
|
-
items.push(stripped);
|
|
134
|
-
}
|
|
135
|
-
return items;
|
|
136
|
-
}
|
|
137
|
-
function collectRoadmapSignals(repoDir) {
|
|
138
|
-
const found = readFirstExisting(repoDir, ['ROADMAP.md', 'docs/ROADMAP.md']);
|
|
139
|
-
if (!found)
|
|
140
|
-
return [];
|
|
141
|
-
return parseListItems(found.content).map((detail) => ({
|
|
142
|
-
source: 'roadmap',
|
|
143
|
-
reference: found.relPath,
|
|
144
|
-
detail,
|
|
145
|
-
}));
|
|
146
|
-
}
|
|
147
|
-
function collectFeedbackSignals(repoDir) {
|
|
148
|
-
const found = readFirstExisting(repoDir, ['FEEDBACK.md', '.factory/feedback.md']);
|
|
149
|
-
if (!found)
|
|
150
|
-
return [];
|
|
151
|
-
return parseListItems(found.content).map((detail) => ({
|
|
152
|
-
source: 'feedback',
|
|
153
|
-
reference: found.relPath,
|
|
154
|
-
detail,
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
function collectTodoSignals(repoDir) {
|
|
158
|
-
const signals = [];
|
|
159
|
-
let filesScanned = 0;
|
|
160
|
-
function walk(dir) {
|
|
161
|
-
if (filesScanned >= MAX_SCANNED_FILES)
|
|
162
|
-
return;
|
|
163
|
-
let entries;
|
|
164
|
-
try {
|
|
165
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
for (const entry of entries) {
|
|
171
|
-
if (filesScanned >= MAX_SCANNED_FILES)
|
|
172
|
-
return;
|
|
173
|
-
if (entry.isDirectory()) {
|
|
174
|
-
if (SKIP_DIRS.has(entry.name))
|
|
175
|
-
continue;
|
|
176
|
-
walk(join(dir, entry.name));
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (!entry.isFile())
|
|
180
|
-
continue;
|
|
181
|
-
if (!TODO_EXTS.has(extname(entry.name)))
|
|
182
|
-
continue;
|
|
183
|
-
const fullPath = join(dir, entry.name);
|
|
184
|
-
filesScanned += 1;
|
|
185
|
-
let size;
|
|
186
|
-
try {
|
|
187
|
-
size = statSync(fullPath).size;
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (size > MAX_FILE_BYTES)
|
|
193
|
-
continue;
|
|
194
|
-
let content;
|
|
195
|
-
try {
|
|
196
|
-
content = readFileSync(fullPath, 'utf-8');
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
const relPath = relative(repoDir, fullPath);
|
|
202
|
-
const lines = content.split('\n');
|
|
203
|
-
for (let i = 0; i < lines.length; i++) {
|
|
204
|
-
const match = /\b(?:TODO|FIXME)\b[:\s-]*(.*)/.exec(lines[i]);
|
|
205
|
-
const detail = match?.[1]?.trim();
|
|
206
|
-
if (!detail)
|
|
207
|
-
continue;
|
|
208
|
-
signals.push({ source: 'code-todo', reference: `${relPath}:${i + 1}`, detail });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
try {
|
|
213
|
-
walk(repoDir);
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
return signals.sort((a, b) => (a.reference < b.reference ? -1 : a.reference > b.reference ? 1 : 0));
|
|
219
|
-
}
|
|
220
|
-
function collectBugThemeSignals(repoDir) {
|
|
221
|
-
let raw;
|
|
222
|
-
try {
|
|
223
|
-
raw = readFileSync(join(repoDir, '.factory', 'fingerprints.json'), 'utf-8');
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
return [];
|
|
227
|
-
}
|
|
228
|
-
let parsed;
|
|
229
|
-
try {
|
|
230
|
-
parsed = JSON.parse(raw);
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
return [];
|
|
234
|
-
}
|
|
235
|
-
const result = FingerprintsSchema.safeParse(parsed);
|
|
236
|
-
if (!result.success)
|
|
237
|
-
return [];
|
|
238
|
-
return result.data.themes
|
|
239
|
-
.filter((theme) => theme.count >= 2)
|
|
240
|
-
.map((theme) => ({
|
|
241
|
-
source: 'bug-theme',
|
|
242
|
-
reference: '.factory/fingerprints.json',
|
|
243
|
-
detail: theme.label,
|
|
244
|
-
count: theme.count,
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
async function collectOpenIssueTitles(repoDir, run) {
|
|
248
|
-
const r = await run(['gh', 'issue', 'list', '--state', 'open', '--limit', '100', '--json', 'title'], {
|
|
249
|
-
cwd: repoDir,
|
|
250
|
-
});
|
|
251
|
-
if (!r.ok)
|
|
252
|
-
return [];
|
|
253
|
-
try {
|
|
254
|
-
const parsed = JSON.parse(r.stdout);
|
|
255
|
-
if (!Array.isArray(parsed))
|
|
256
|
-
return [];
|
|
257
|
-
return parsed
|
|
258
|
-
.map((item) => item.title)
|
|
259
|
-
.filter((title) => typeof title === 'string');
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
return [];
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// ---------- Candidate building ----------
|
|
266
|
-
function buildHypothesis(signal) {
|
|
267
|
-
switch (signal.source) {
|
|
268
|
-
case 'code-todo':
|
|
269
|
-
return `Resolve TODO in ${signal.reference}: ${signal.detail}`;
|
|
270
|
-
case 'roadmap':
|
|
271
|
-
return `Advance roadmap item: ${signal.detail}`;
|
|
272
|
-
case 'feedback':
|
|
273
|
-
return `Act on user feedback: ${signal.detail}`;
|
|
274
|
-
case 'bug-theme':
|
|
275
|
-
return `Harden against recurring failures: ${signal.detail} (seen ${signal.count ?? 1}×)`;
|
|
276
|
-
case 'constitution':
|
|
277
|
-
throw new Error('constitution signals do not generate candidates directly');
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function makeCandidate(signal, keywords, constitutionSignal) {
|
|
281
|
-
const matchesNorthStar = constitutionSignal !== undefined && tokenize(signal.detail).some((token) => keywords.has(token));
|
|
282
|
-
const base = SOURCE_WEIGHT[signal.source];
|
|
283
|
-
const recurrence = signal.source === 'bug-theme' ? Math.min(5, Math.max(0, (signal.count ?? 1) - 1)) : 0;
|
|
284
|
-
const northStar = matchesNorthStar ? 2 : 0;
|
|
285
|
-
const signals = [signal];
|
|
286
|
-
if (matchesNorthStar && constitutionSignal)
|
|
287
|
-
signals.push(constitutionSignal);
|
|
288
|
-
return {
|
|
289
|
-
hypothesis: buildHypothesis(signal),
|
|
290
|
-
score: base + recurrence + northStar,
|
|
291
|
-
signals,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function compareCandidates(a, b) {
|
|
295
|
-
if (b.score !== a.score)
|
|
296
|
-
return b.score - a.score;
|
|
297
|
-
const aPrimary = a.signals[0];
|
|
298
|
-
const bPrimary = b.signals[0];
|
|
299
|
-
const priorityDiff = SOURCE_PRIORITY[bPrimary.source] - SOURCE_PRIORITY[aPrimary.source];
|
|
300
|
-
if (priorityDiff !== 0)
|
|
301
|
-
return priorityDiff;
|
|
302
|
-
if (aPrimary.reference !== bPrimary.reference)
|
|
303
|
-
return aPrimary.reference < bPrimary.reference ? -1 : 1;
|
|
304
|
-
if (a.hypothesis !== b.hypothesis)
|
|
305
|
-
return a.hypothesis < b.hypothesis ? -1 : 1;
|
|
306
|
-
return 0;
|
|
307
|
-
}
|
|
308
|
-
// ---------- Main entry point ----------
|
|
309
|
-
export async function runDiscoveryScan(options, deps = {}) {
|
|
310
|
-
const now = deps.now ?? (() => new Date());
|
|
311
|
-
const run = deps.run ??
|
|
312
|
-
((argv, o) => existsSync(join(o.cwd, '.git'))
|
|
313
|
-
? runCommand(argv, { cwd: o.cwd, timeoutMs: OPEN_ISSUES_TIMEOUT_MS })
|
|
314
|
-
: Promise.resolve({ stdout: '', ok: false }));
|
|
315
|
-
const cap = Math.max(0, Math.floor(options.maxCandidates ?? DEFAULT_MAX_CANDIDATES));
|
|
316
|
-
const { repoDir } = options;
|
|
317
|
-
const constitutionSignals = collectConstitutionSignals(repoDir);
|
|
318
|
-
const roadmapSignals = collectRoadmapSignals(repoDir);
|
|
319
|
-
const todoSignals = collectTodoSignals(repoDir);
|
|
320
|
-
const feedbackSignals = collectFeedbackSignals(repoDir);
|
|
321
|
-
const bugThemeSignals = collectBugThemeSignals(repoDir);
|
|
322
|
-
const openIssueTitles = await collectOpenIssueTitles(repoDir, run);
|
|
323
|
-
const signalsCollected = constitutionSignals.length +
|
|
324
|
-
roadmapSignals.length +
|
|
325
|
-
todoSignals.length +
|
|
326
|
-
feedbackSignals.length +
|
|
327
|
-
bugThemeSignals.length;
|
|
328
|
-
const keywords = northStarKeywords(constitutionSignals);
|
|
329
|
-
const constitutionSignal = constitutionSignals[0];
|
|
330
|
-
const generatingSignals = [...roadmapSignals, ...todoSignals, ...feedbackSignals, ...bugThemeSignals];
|
|
331
|
-
const candidates = generatingSignals
|
|
332
|
-
.filter((signal) => !sharesSignificantToken(signal.detail, openIssueTitles))
|
|
333
|
-
.map((signal) => makeCandidate(signal, keywords, constitutionSignal))
|
|
334
|
-
.sort(compareCandidates);
|
|
335
|
-
return {
|
|
336
|
-
scannedAt: now().toISOString(),
|
|
337
|
-
repoDir,
|
|
338
|
-
signalsCollected,
|
|
339
|
-
candidates: candidates.slice(0, cap),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
//# sourceMappingURL=index.js.map
|