@shrkcrft/context-planner 0.1.0-alpha.10
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 +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/intent/benchmark.d.ts +56 -0
- package/dist/intent/benchmark.d.ts.map +1 -0
- package/dist/intent/benchmark.js +79 -0
- package/dist/intent/classify-intent.d.ts +13 -0
- package/dist/intent/classify-intent.d.ts.map +1 -0
- package/dist/intent/classify-intent.js +42 -0
- package/dist/intent/starter-benchmark.d.ts +13 -0
- package/dist/intent/starter-benchmark.d.ts.map +1 -0
- package/dist/intent/starter-benchmark.js +43 -0
- package/dist/planner/plan-context.d.ts +29 -0
- package/dist/planner/plan-context.d.ts.map +1 -0
- package/dist/planner/plan-context.js +219 -0
- package/dist/ranker/score-files.d.ts +45 -0
- package/dist/ranker/score-files.d.ts.map +1 -0
- package/dist/ranker/score-files.js +112 -0
- package/dist/schema/context-pack.d.ts +74 -0
- package/dist/schema/context-pack.d.ts.map +1 -0
- package/dist/schema/context-pack.js +9 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './schema/context-pack.js';
|
|
2
|
+
export * from './intent/classify-intent.js';
|
|
3
|
+
export * from './intent/benchmark.js';
|
|
4
|
+
export * from './intent/starter-benchmark.js';
|
|
5
|
+
export * from './ranker/score-files.js';
|
|
6
|
+
export * from './planner/plan-context.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TaskIntent } from '../schema/context-pack.js';
|
|
2
|
+
export declare const INTENT_BENCHMARK_SCHEMA: "sharkcraft.intent-benchmark/v1";
|
|
3
|
+
/**
|
|
4
|
+
* One labelled task → expected-intent pair. The benchmark runs every
|
|
5
|
+
* case through `classifyIntent` and reports per-case + aggregate
|
|
6
|
+
* accuracy. Optional `notes` lets authors leave a hint about why a
|
|
7
|
+
* particular phrasing was added (often after a regression).
|
|
8
|
+
*/
|
|
9
|
+
export interface IIntentBenchmarkCase {
|
|
10
|
+
task: string;
|
|
11
|
+
expected: TaskIntent;
|
|
12
|
+
notes?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IIntentBenchmark {
|
|
15
|
+
schema: typeof INTENT_BENCHMARK_SCHEMA;
|
|
16
|
+
cases: readonly IIntentBenchmarkCase[];
|
|
17
|
+
}
|
|
18
|
+
export interface IIntentBenchmarkRunCase {
|
|
19
|
+
task: string;
|
|
20
|
+
expected: TaskIntent;
|
|
21
|
+
actual: TaskIntent;
|
|
22
|
+
passed: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface IIntentBenchmarkRun {
|
|
25
|
+
schema: typeof INTENT_BENCHMARK_SCHEMA;
|
|
26
|
+
total: number;
|
|
27
|
+
passed: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
/** Accuracy in [0, 1]. */
|
|
30
|
+
accuracy: number;
|
|
31
|
+
cases: readonly IIntentBenchmarkRunCase[];
|
|
32
|
+
/** ISO timestamp the run completed. */
|
|
33
|
+
ranAt: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the benchmark fixture from `sharkcraft/intent-benchmark.json`
|
|
37
|
+
* (NOT under `.sharkcraft/` — this is an author-provided, checked-in
|
|
38
|
+
* fixture, not derived state). Returns undefined when missing or when
|
|
39
|
+
* the file is unparseable; the caller should treat that as "no
|
|
40
|
+
* benchmark configured" rather than an error.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadIntentBenchmark(projectRoot: string): IIntentBenchmark | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Run the benchmark in-process. Pure — no side effects, no I/O beyond
|
|
45
|
+
* the input. Callers persist the run via `writeBenchmarkRun` when they
|
|
46
|
+
* want the doctor to surface it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runIntentBenchmark(benchmark: IIntentBenchmark): IIntentBenchmarkRun;
|
|
49
|
+
export declare const INTENT_BENCHMARK_RUN_REL: ".sharkcraft/context-planner/intent-benchmark.json";
|
|
50
|
+
/**
|
|
51
|
+
* Persist a benchmark run so the doctor can surface accuracy without
|
|
52
|
+
* re-running the fixture on every doctor invocation.
|
|
53
|
+
*/
|
|
54
|
+
export declare function writeBenchmarkRun(projectRoot: string, run: IIntentBenchmarkRun): string;
|
|
55
|
+
export declare function readBenchmarkRun(projectRoot: string): IIntentBenchmarkRun | undefined;
|
|
56
|
+
//# sourceMappingURL=benchmark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmark.d.ts","sourceRoot":"","sources":["../../src/intent/benchmark.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,eAAO,MAAM,uBAAuB,EAAG,gCAAyC,CAAC;AAEjF;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,uBAAuB,CAAC;IACvC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,CAAC;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,OAAO,uBAAuB,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC1C,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAWrF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,GAC1B,mBAAmB,CAmBrB;AAED,eAAO,MAAM,wBAAwB,EACnC,mDAA4D,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,mBAAmB,GACvB,MAAM,CAKR;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAClB,mBAAmB,GAAG,SAAS,CAUjC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { classifyIntent } from "./classify-intent.js";
|
|
4
|
+
export const INTENT_BENCHMARK_SCHEMA = 'sharkcraft.intent-benchmark/v1';
|
|
5
|
+
const BENCHMARK_REL = 'sharkcraft/intent-benchmark.json';
|
|
6
|
+
/**
|
|
7
|
+
* Read the benchmark fixture from `sharkcraft/intent-benchmark.json`
|
|
8
|
+
* (NOT under `.sharkcraft/` — this is an author-provided, checked-in
|
|
9
|
+
* fixture, not derived state). Returns undefined when missing or when
|
|
10
|
+
* the file is unparseable; the caller should treat that as "no
|
|
11
|
+
* benchmark configured" rather than an error.
|
|
12
|
+
*/
|
|
13
|
+
export function loadIntentBenchmark(projectRoot) {
|
|
14
|
+
const abs = nodePath.join(projectRoot, BENCHMARK_REL);
|
|
15
|
+
if (!existsSync(abs))
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(readFileSync(abs, 'utf8'));
|
|
19
|
+
if (raw.schema !== INTENT_BENCHMARK_SCHEMA)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!Array.isArray(raw.cases))
|
|
22
|
+
return undefined;
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run the benchmark in-process. Pure — no side effects, no I/O beyond
|
|
31
|
+
* the input. Callers persist the run via `writeBenchmarkRun` when they
|
|
32
|
+
* want the doctor to surface it.
|
|
33
|
+
*/
|
|
34
|
+
export function runIntentBenchmark(benchmark) {
|
|
35
|
+
const cases = [];
|
|
36
|
+
let passed = 0;
|
|
37
|
+
for (const c of benchmark.cases) {
|
|
38
|
+
const actual = classifyIntent(c.task);
|
|
39
|
+
const ok = actual === c.expected;
|
|
40
|
+
if (ok)
|
|
41
|
+
passed += 1;
|
|
42
|
+
cases.push({ task: c.task, expected: c.expected, actual, passed: ok });
|
|
43
|
+
}
|
|
44
|
+
const total = benchmark.cases.length;
|
|
45
|
+
return {
|
|
46
|
+
schema: INTENT_BENCHMARK_SCHEMA,
|
|
47
|
+
total,
|
|
48
|
+
passed,
|
|
49
|
+
failed: total - passed,
|
|
50
|
+
accuracy: total === 0 ? 1 : passed / total,
|
|
51
|
+
cases,
|
|
52
|
+
ranAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export const INTENT_BENCHMARK_RUN_REL = '.sharkcraft/context-planner/intent-benchmark.json';
|
|
56
|
+
/**
|
|
57
|
+
* Persist a benchmark run so the doctor can surface accuracy without
|
|
58
|
+
* re-running the fixture on every doctor invocation.
|
|
59
|
+
*/
|
|
60
|
+
export function writeBenchmarkRun(projectRoot, run) {
|
|
61
|
+
const abs = nodePath.join(projectRoot, INTENT_BENCHMARK_RUN_REL);
|
|
62
|
+
mkdirSync(nodePath.dirname(abs), { recursive: true });
|
|
63
|
+
writeFileSync(abs, JSON.stringify(run, null, 2), 'utf8');
|
|
64
|
+
return abs;
|
|
65
|
+
}
|
|
66
|
+
export function readBenchmarkRun(projectRoot) {
|
|
67
|
+
const abs = nodePath.join(projectRoot, INTENT_BENCHMARK_RUN_REL);
|
|
68
|
+
if (!existsSync(abs))
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const raw = JSON.parse(readFileSync(abs, 'utf8'));
|
|
72
|
+
if (raw.schema !== INTENT_BENCHMARK_SCHEMA)
|
|
73
|
+
return undefined;
|
|
74
|
+
return raw;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TaskIntent } from '../schema/context-pack.js';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic, keyword-based task intent classifier.
|
|
4
|
+
*
|
|
5
|
+
* No embedded model. Maps free-text task strings to an intent label so
|
|
6
|
+
* the ranker can tune its weights. Conflicts resolve in priority order:
|
|
7
|
+
* release > migration > bug-fix > refactor > docs > feature
|
|
8
|
+
*
|
|
9
|
+
* `unknown` is returned when no keyword matches — the ranker then uses
|
|
10
|
+
* its neutral baseline weights.
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyIntent(task: string): TaskIntent;
|
|
13
|
+
//# sourceMappingURL=classify-intent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify-intent.d.ts","sourceRoot":"","sources":["../../src/intent/classify-intent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAoBvD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, keyword-based task intent classifier.
|
|
3
|
+
*
|
|
4
|
+
* No embedded model. Maps free-text task strings to an intent label so
|
|
5
|
+
* the ranker can tune its weights. Conflicts resolve in priority order:
|
|
6
|
+
* release > migration > bug-fix > refactor > docs > feature
|
|
7
|
+
*
|
|
8
|
+
* `unknown` is returned when no keyword matches — the ranker then uses
|
|
9
|
+
* its neutral baseline weights.
|
|
10
|
+
*/
|
|
11
|
+
export function classifyIntent(task) {
|
|
12
|
+
const t = task.toLowerCase();
|
|
13
|
+
if (matchAny(t, ['release', 'cut release', 'publish', 'preflight', 'tag ']))
|
|
14
|
+
return 'release';
|
|
15
|
+
if (matchAny(t, ['migration', 'migrate', 'upgrade', 'deprecat']))
|
|
16
|
+
return 'migration';
|
|
17
|
+
if (matchAny(t, ['bug', 'fix', 'error', 'broken', 'regression', 'crash', 'throws', 'fails']))
|
|
18
|
+
return 'bug-fix';
|
|
19
|
+
if (matchAny(t, ['refactor', 'cleanup', 'simplify', 'extract', 'rename', 'move']))
|
|
20
|
+
return 'refactor';
|
|
21
|
+
if (matchAny(t, ['docs', 'document', 'readme', 'guide', 'tutorial', 'explain']))
|
|
22
|
+
return 'docs';
|
|
23
|
+
if (matchAny(t, [
|
|
24
|
+
'add ',
|
|
25
|
+
'create ',
|
|
26
|
+
'implement',
|
|
27
|
+
'feature',
|
|
28
|
+
'support for',
|
|
29
|
+
'enable ',
|
|
30
|
+
'introduce',
|
|
31
|
+
'new ',
|
|
32
|
+
]))
|
|
33
|
+
return 'feature';
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
function matchAny(t, needles) {
|
|
37
|
+
for (const n of needles) {
|
|
38
|
+
if (t.includes(n))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type IIntentBenchmark } from './benchmark.js';
|
|
2
|
+
/**
|
|
3
|
+
* Curated starter benchmark covering the canonical phrasings the
|
|
4
|
+
* classifier should handle on day one. Used by `shrk context
|
|
5
|
+
* benchmark seed` to bootstrap a fresh fixture. Authors are expected
|
|
6
|
+
* to grow the set per project — especially when a new task verb
|
|
7
|
+
* starts cropping up in real PRs but lands in `unknown`.
|
|
8
|
+
*
|
|
9
|
+
* Conflict-resolution priority in the classifier today:
|
|
10
|
+
* release > migration > bug-fix > refactor > docs > feature
|
|
11
|
+
*/
|
|
12
|
+
export declare const STARTER_INTENT_BENCHMARK: IIntentBenchmark;
|
|
13
|
+
//# sourceMappingURL=starter-benchmark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter-benchmark.d.ts","sourceRoot":"","sources":["../../src/intent/starter-benchmark.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEhF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,EAAE,gBAoCtC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { INTENT_BENCHMARK_SCHEMA } from "./benchmark.js";
|
|
2
|
+
/**
|
|
3
|
+
* Curated starter benchmark covering the canonical phrasings the
|
|
4
|
+
* classifier should handle on day one. Used by `shrk context
|
|
5
|
+
* benchmark seed` to bootstrap a fresh fixture. Authors are expected
|
|
6
|
+
* to grow the set per project — especially when a new task verb
|
|
7
|
+
* starts cropping up in real PRs but lands in `unknown`.
|
|
8
|
+
*
|
|
9
|
+
* Conflict-resolution priority in the classifier today:
|
|
10
|
+
* release > migration > bug-fix > refactor > docs > feature
|
|
11
|
+
*/
|
|
12
|
+
export const STARTER_INTENT_BENCHMARK = {
|
|
13
|
+
schema: INTENT_BENCHMARK_SCHEMA,
|
|
14
|
+
cases: [
|
|
15
|
+
// bug-fix
|
|
16
|
+
{ task: 'fix the broken login flow', expected: 'bug-fix' },
|
|
17
|
+
{ task: 'auth crashes when password is empty', expected: 'bug-fix' },
|
|
18
|
+
{ task: 'regression on date parsing', expected: 'bug-fix' },
|
|
19
|
+
{ task: 'NPE in the dashboard renderer', expected: 'bug-fix', notes: 'NPE keyword maps via `error`' },
|
|
20
|
+
// feature
|
|
21
|
+
{ task: 'add a dark mode toggle to settings', expected: 'feature' },
|
|
22
|
+
{ task: 'create a new export-as-csv command', expected: 'feature' },
|
|
23
|
+
{ task: 'implement webhook delivery retries', expected: 'feature' },
|
|
24
|
+
{ task: 'introduce per-tenant rate limiting', expected: 'feature' },
|
|
25
|
+
// refactor
|
|
26
|
+
{ task: 'refactor the dashboard panel registry', expected: 'refactor' },
|
|
27
|
+
{ task: 'rename `processRow` to `applyRow`', expected: 'refactor' },
|
|
28
|
+
{ task: 'extract auth helpers into their own module', expected: 'refactor' },
|
|
29
|
+
{ task: 'simplify the search-index builder', expected: 'refactor' },
|
|
30
|
+
// docs
|
|
31
|
+
{ task: 'update the README quickstart', expected: 'docs' },
|
|
32
|
+
{ task: 'document the new context-planner API', expected: 'docs' },
|
|
33
|
+
{ task: 'write a guide for arch contracts', expected: 'docs' },
|
|
34
|
+
// release
|
|
35
|
+
{ task: 'cut release 0.2.0', expected: 'release' },
|
|
36
|
+
{ task: 'run release preflight', expected: 'release' },
|
|
37
|
+
{ task: 'publish alpha tag for the cli package', expected: 'release' },
|
|
38
|
+
// migration
|
|
39
|
+
{ task: 'migrate from joi to zod', expected: 'migration' },
|
|
40
|
+
{ task: 'upgrade typescript to 5.6', expected: 'migration' },
|
|
41
|
+
{ task: 'deprecate the legacy boundary API', expected: 'migration' },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type IContextPack } from '../schema/context-pack.js';
|
|
2
|
+
export interface IPlanContextOptions {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
/** Free-text task description. */
|
|
5
|
+
task: string;
|
|
6
|
+
/** Hard token budget. Default 8000. */
|
|
7
|
+
budgetTokens?: number;
|
|
8
|
+
/** Pre-selected file hints (will be boosted in ranking). */
|
|
9
|
+
hintedFiles?: readonly string[];
|
|
10
|
+
/** Pre-selected package hints (path prefix `packages/<x>`). */
|
|
11
|
+
hintedPackages?: readonly string[];
|
|
12
|
+
/** Cap on returned files. Default 30 (after budget pruning). */
|
|
13
|
+
maxFiles?: number;
|
|
14
|
+
/**
|
|
15
|
+
* If true and the bridge isn't built, fall back to no rule/path/template
|
|
16
|
+
* info (still produces a useful pack). Default true.
|
|
17
|
+
*/
|
|
18
|
+
tolerateMissingBridge?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Produce a deterministic context pack for an AI coding agent.
|
|
22
|
+
*
|
|
23
|
+
* Inputs are graph + bridge + free-text task. Output fits the requested
|
|
24
|
+
* token budget within a small tolerance. The same task on the same repo
|
|
25
|
+
* returns the same pack — a property that lets agents share / replay
|
|
26
|
+
* context.
|
|
27
|
+
*/
|
|
28
|
+
export declare function planContext(options: IPlanContextOptions): IContextPack;
|
|
29
|
+
//# sourceMappingURL=plan-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-context.d.ts","sourceRoot":"","sources":["../../src/planner/plan-context.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,YAAY,EAMlB,MAAM,2BAA2B,CAAC;AAInC,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAwGtE"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { GraphQueryApi, GraphStore, } from '@shrkcrft/graph';
|
|
2
|
+
import { BridgeStore, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
|
|
3
|
+
import { CONTEXT_PACK_SCHEMA, } from "../schema/context-pack.js";
|
|
4
|
+
import { classifyIntent } from "../intent/classify-intent.js";
|
|
5
|
+
import { scoreFiles } from "../ranker/score-files.js";
|
|
6
|
+
/**
|
|
7
|
+
* Produce a deterministic context pack for an AI coding agent.
|
|
8
|
+
*
|
|
9
|
+
* Inputs are graph + bridge + free-text task. Output fits the requested
|
|
10
|
+
* token budget within a small tolerance. The same task on the same repo
|
|
11
|
+
* returns the same pack — a property that lets agents share / replay
|
|
12
|
+
* context.
|
|
13
|
+
*/
|
|
14
|
+
export function planContext(options) {
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
const budgetTokens = options.budgetTokens ?? 8000;
|
|
17
|
+
const maxFiles = options.maxFiles ?? 30;
|
|
18
|
+
const tolerate = options.tolerateMissingBridge ?? true;
|
|
19
|
+
const graphStore = new GraphStore(options.projectRoot);
|
|
20
|
+
if (!graphStore.exists()) {
|
|
21
|
+
diagnostics.push("code-graph store missing — run `shrk graph index`");
|
|
22
|
+
return emptyPack(options.task, budgetTokens, diagnostics);
|
|
23
|
+
}
|
|
24
|
+
const api = GraphQueryApi.fromStore(options.projectRoot);
|
|
25
|
+
const intent = classifyIntent(options.task);
|
|
26
|
+
// 1. Rank.
|
|
27
|
+
const scored = scoreFiles(api, {
|
|
28
|
+
task: options.task,
|
|
29
|
+
intent,
|
|
30
|
+
...(options.hintedFiles ? { hintedFiles: options.hintedFiles } : {}),
|
|
31
|
+
...(options.hintedPackages ? { hintedPackages: options.hintedPackages } : {}),
|
|
32
|
+
});
|
|
33
|
+
// 2. Estimate tokens (deterministic, BPE-ish approximation).
|
|
34
|
+
const ranked = scored
|
|
35
|
+
.slice(0, maxFiles * 3) // start with a generous pre-pool before budget pruning
|
|
36
|
+
.map((s) => toRankedFile(s, api));
|
|
37
|
+
// 3. Token-budget pruning. Greedy include in score order until budget filled.
|
|
38
|
+
const accepted = [];
|
|
39
|
+
let used = 0;
|
|
40
|
+
let truncated = false;
|
|
41
|
+
for (const f of ranked) {
|
|
42
|
+
if (accepted.length >= maxFiles)
|
|
43
|
+
break;
|
|
44
|
+
if (used + f.estimatedTokens > budgetTokens) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
accepted.push(f);
|
|
49
|
+
used += f.estimatedTokens;
|
|
50
|
+
}
|
|
51
|
+
if (scored.length > accepted.length)
|
|
52
|
+
truncated = true;
|
|
53
|
+
// 4. Bridge lookups for the selected file set.
|
|
54
|
+
let rules = [];
|
|
55
|
+
let paths = [];
|
|
56
|
+
let templates = [];
|
|
57
|
+
const bridgeStore = new BridgeStore(options.projectRoot);
|
|
58
|
+
if (bridgeStore.exists()) {
|
|
59
|
+
const bridgeApi = RuleGraphQueryApi.fromStores(options.projectRoot);
|
|
60
|
+
const seenRule = new Set();
|
|
61
|
+
const seenPath = new Set();
|
|
62
|
+
const seenTpl = new Set();
|
|
63
|
+
for (const f of accepted) {
|
|
64
|
+
const view = bridgeApi.forFile(f.path);
|
|
65
|
+
if (!view)
|
|
66
|
+
continue;
|
|
67
|
+
for (const h of view.rules) {
|
|
68
|
+
if (seenRule.has(h.target.id))
|
|
69
|
+
continue;
|
|
70
|
+
seenRule.add(h.target.id);
|
|
71
|
+
rules.push({
|
|
72
|
+
id: h.target.id,
|
|
73
|
+
label: h.target.label,
|
|
74
|
+
severity: h.edge.data?.['severity'] ?? undefined,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const h of view.paths) {
|
|
78
|
+
if (seenPath.has(h.target.id))
|
|
79
|
+
continue;
|
|
80
|
+
seenPath.add(h.target.id);
|
|
81
|
+
paths.push({ id: h.target.id, label: h.target.label });
|
|
82
|
+
}
|
|
83
|
+
for (const h of view.templates) {
|
|
84
|
+
if (seenTpl.has(h.target.id))
|
|
85
|
+
continue;
|
|
86
|
+
seenTpl.add(h.target.id);
|
|
87
|
+
templates.push({ id: h.target.id, label: h.target.label });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (tolerate) {
|
|
92
|
+
diagnostics.push("bridge store missing — rules/paths/templates omitted (run `shrk rule-graph index`)");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
diagnostics.push("bridge store missing");
|
|
96
|
+
}
|
|
97
|
+
// 5. Likely tests for the selected file set (importers + co-located).
|
|
98
|
+
const tests = collectTests(api, accepted);
|
|
99
|
+
// 6. Surface risks: cross-package edges, public-API touches, cycles in the selected set.
|
|
100
|
+
const risks = collectRisks(api, accepted);
|
|
101
|
+
// 7. Do-not-touch zones — generated files, vendored, dist, lock.
|
|
102
|
+
const doNotTouch = computeDoNotTouch(accepted);
|
|
103
|
+
return {
|
|
104
|
+
schema: CONTEXT_PACK_SCHEMA,
|
|
105
|
+
intent,
|
|
106
|
+
task: options.task,
|
|
107
|
+
files: accepted,
|
|
108
|
+
rules,
|
|
109
|
+
paths,
|
|
110
|
+
templates,
|
|
111
|
+
tests,
|
|
112
|
+
risks,
|
|
113
|
+
doNotTouch,
|
|
114
|
+
budget: { requested: budgetTokens, used, truncated },
|
|
115
|
+
diagnostics,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function toRankedFile(s, api) {
|
|
119
|
+
const node = s.node;
|
|
120
|
+
// Token estimate: 1 token ≈ 4 chars, plus a small fixed overhead.
|
|
121
|
+
const sizeBytes = node.data?.['sizeBytes'] ?? 2000;
|
|
122
|
+
const estimatedTokens = Math.max(40, Math.ceil(sizeBytes / 4));
|
|
123
|
+
// Bound the score to [0, 1].
|
|
124
|
+
const bounded = Math.max(0, Math.min(1, s.score / 3));
|
|
125
|
+
void api;
|
|
126
|
+
return {
|
|
127
|
+
path: node.path,
|
|
128
|
+
nodeId: node.id,
|
|
129
|
+
score: Math.round(bounded * 100) / 100,
|
|
130
|
+
estimatedTokens,
|
|
131
|
+
reasons: s.reasons,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function collectTests(api, files) {
|
|
135
|
+
const out = new Set();
|
|
136
|
+
for (const f of files) {
|
|
137
|
+
// Co-located: `__tests__/<name>.test.ts` or `<base>.test.ts`.
|
|
138
|
+
const candidates = guessTestPaths(f.path);
|
|
139
|
+
for (const c of candidates) {
|
|
140
|
+
const t = api.findFile(c);
|
|
141
|
+
if (t)
|
|
142
|
+
out.add(t.path);
|
|
143
|
+
}
|
|
144
|
+
// Importer tests: any file tagged `test` that imports this one.
|
|
145
|
+
for (const importer of api.importersOf(f.nodeId)) {
|
|
146
|
+
if (!(importer.tags ?? []).includes('test'))
|
|
147
|
+
continue;
|
|
148
|
+
if (importer.path)
|
|
149
|
+
out.add(importer.path);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...out].sort().slice(0, 50);
|
|
153
|
+
}
|
|
154
|
+
function guessTestPaths(path) {
|
|
155
|
+
// `packages/foo/src/bar.ts` → check `__tests__/bar.test.ts`, `bar.test.ts`, etc.
|
|
156
|
+
const out = [];
|
|
157
|
+
const lastSlash = path.lastIndexOf('/');
|
|
158
|
+
const dir = lastSlash >= 0 ? path.slice(0, lastSlash) : '.';
|
|
159
|
+
const base = (lastSlash >= 0 ? path.slice(lastSlash + 1) : path).replace(/\.[tj]sx?$/, '');
|
|
160
|
+
for (const ext of ['.test.ts', '.test.tsx', '.spec.ts']) {
|
|
161
|
+
out.push(`${dir}/__tests__/${base}${ext}`);
|
|
162
|
+
out.push(`${dir}/${base}${ext}`);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function collectRisks(api, files) {
|
|
167
|
+
const out = [];
|
|
168
|
+
let publicApiCount = 0;
|
|
169
|
+
let crossPackageCount = 0;
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
if (/\/index\.ts$/.test(f.path) || /^index\.ts$/.test(f.path) || f.path.endsWith('.d.ts')) {
|
|
172
|
+
publicApiCount += 1;
|
|
173
|
+
}
|
|
174
|
+
for (const importer of api.importersOf(f.nodeId)) {
|
|
175
|
+
const pkgImporter = importer.path?.split('/').slice(0, 2).join('/');
|
|
176
|
+
const pkgFile = f.path.split('/').slice(0, 2).join('/');
|
|
177
|
+
if (pkgImporter && pkgFile && pkgImporter !== pkgFile)
|
|
178
|
+
crossPackageCount += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (publicApiCount > 0) {
|
|
182
|
+
out.push({
|
|
183
|
+
kind: 'public-api',
|
|
184
|
+
label: `${publicApiCount} selected file(s) are public-API entrypoints — changes ripple to consumers`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (crossPackageCount >= 5) {
|
|
188
|
+
out.push({
|
|
189
|
+
kind: 'cross-package',
|
|
190
|
+
label: `${crossPackageCount} cross-package importers of selected files`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function computeDoNotTouch(files) {
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const f of files) {
|
|
198
|
+
if (/\bdist\b|\bbuild\b|\.generated\.|\.lock$/.test(f.path)) {
|
|
199
|
+
out.push(f.path);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
function emptyPack(task, budget, diagnostics) {
|
|
205
|
+
return {
|
|
206
|
+
schema: CONTEXT_PACK_SCHEMA,
|
|
207
|
+
intent: classifyIntent(task),
|
|
208
|
+
task,
|
|
209
|
+
files: [],
|
|
210
|
+
rules: [],
|
|
211
|
+
paths: [],
|
|
212
|
+
templates: [],
|
|
213
|
+
tests: [],
|
|
214
|
+
risks: [],
|
|
215
|
+
doNotTouch: [],
|
|
216
|
+
budget: { requested: budget, used: 0, truncated: false },
|
|
217
|
+
diagnostics,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GraphQueryApi, type INode } from '@shrkcrft/graph';
|
|
2
|
+
import type { TaskIntent } from '../schema/context-pack.js';
|
|
3
|
+
export interface IScoredFile {
|
|
4
|
+
node: INode;
|
|
5
|
+
score: number;
|
|
6
|
+
reasons: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface IScoreInput {
|
|
9
|
+
/** Free-text task. */
|
|
10
|
+
task: string;
|
|
11
|
+
/** Classified intent (drives weights). */
|
|
12
|
+
intent: TaskIntent;
|
|
13
|
+
/** Optional file hints from the caller (pre-selected). */
|
|
14
|
+
hintedFiles?: readonly string[];
|
|
15
|
+
/** Optional package hints. */
|
|
16
|
+
hintedPackages?: readonly string[];
|
|
17
|
+
/** Per-package weight multipliers from intent. */
|
|
18
|
+
weights?: Partial<IRankerWeights>;
|
|
19
|
+
}
|
|
20
|
+
export interface IRankerWeights {
|
|
21
|
+
/** Match between task keywords and file path. */
|
|
22
|
+
pathKeyword: number;
|
|
23
|
+
/** Match between task keywords and any declared symbol in the file. */
|
|
24
|
+
symbolKeyword: number;
|
|
25
|
+
/** File is among the caller's hints. */
|
|
26
|
+
hintedFile: number;
|
|
27
|
+
/** File belongs to a hinted package. */
|
|
28
|
+
hintedPackage: number;
|
|
29
|
+
/** File is a test for a ranked source file (followed-up only). */
|
|
30
|
+
testFollowup: number;
|
|
31
|
+
/** Penalty for generated files (subtract). */
|
|
32
|
+
generatedPenalty: number;
|
|
33
|
+
/** Penalty for test files when intent != bug-fix. */
|
|
34
|
+
testPenalty: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rank every file node in the graph against the task. Returns scored
|
|
38
|
+
* files sorted descending by score, with explicit reasons attached so
|
|
39
|
+
* the agent can see why a file was picked.
|
|
40
|
+
*
|
|
41
|
+
* Deterministic — no LLM calls. Weights are intent-tunable but the
|
|
42
|
+
* keyword extractor and the scoring formula are pure.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scoreFiles(api: GraphQueryApi, input: IScoreInput): readonly IScoredFile[];
|
|
45
|
+
//# sourceMappingURL=score-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-files.d.ts","sourceRoot":"","sources":["../../src/ranker/score-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,UAAU,CAAC;IACnB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,8BAA8B;IAC9B,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,kDAAkD;IAClD,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,gBAAgB,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;CACrB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,WAAW,EAAE,CAkDzF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const DEFAULT_WEIGHTS = {
|
|
2
|
+
pathKeyword: 1.0,
|
|
3
|
+
symbolKeyword: 0.7,
|
|
4
|
+
hintedFile: 2.0,
|
|
5
|
+
hintedPackage: 0.5,
|
|
6
|
+
testFollowup: 0.4,
|
|
7
|
+
generatedPenalty: 0.6,
|
|
8
|
+
testPenalty: 0.3,
|
|
9
|
+
};
|
|
10
|
+
const STOPWORDS = new Set([
|
|
11
|
+
'a', 'an', 'and', 'or', 'the', 'of', 'for', 'to', 'in', 'on', 'with', 'add', 'make', 'do',
|
|
12
|
+
'is', 'are', 'be', 'as', 'at', 'by', 'from', 'that', 'this', 'it', 'i', 'we', 'our', 'your',
|
|
13
|
+
'should', 'want', 'need', 'please', 'can', 'could', 'would', 'use', 'using', 'also',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Rank every file node in the graph against the task. Returns scored
|
|
17
|
+
* files sorted descending by score, with explicit reasons attached so
|
|
18
|
+
* the agent can see why a file was picked.
|
|
19
|
+
*
|
|
20
|
+
* Deterministic — no LLM calls. Weights are intent-tunable but the
|
|
21
|
+
* keyword extractor and the scoring formula are pure.
|
|
22
|
+
*/
|
|
23
|
+
export function scoreFiles(api, input) {
|
|
24
|
+
const weights = { ...DEFAULT_WEIGHTS, ...applyIntentTuning(input.intent), ...input.weights };
|
|
25
|
+
const keywords = extractKeywords(input.task);
|
|
26
|
+
const hintedFiles = new Set(input.hintedFiles ?? []);
|
|
27
|
+
const hintedPackages = new Set(input.hintedPackages ?? []);
|
|
28
|
+
const candidates = [];
|
|
29
|
+
for (const node of api.allFiles()) {
|
|
30
|
+
const reasons = [];
|
|
31
|
+
let score = 0;
|
|
32
|
+
if (hintedFiles.has(node.path)) {
|
|
33
|
+
score += weights.hintedFile;
|
|
34
|
+
reasons.push('hinted file');
|
|
35
|
+
}
|
|
36
|
+
if (hintedPackages.size > 0) {
|
|
37
|
+
const pkg = node.path.split('/').slice(0, 2).join('/');
|
|
38
|
+
if (hintedPackages.has(pkg)) {
|
|
39
|
+
score += weights.hintedPackage;
|
|
40
|
+
reasons.push('hinted package');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (keywords.size > 0) {
|
|
44
|
+
const pathHits = countKeywordHits(node.path, keywords);
|
|
45
|
+
if (pathHits > 0) {
|
|
46
|
+
score += weights.pathKeyword * Math.min(1, pathHits / 2);
|
|
47
|
+
reasons.push(`path matches ${pathHits} keyword(s)`);
|
|
48
|
+
}
|
|
49
|
+
// Symbol-name matches.
|
|
50
|
+
const symbolMatches = api.symbolsIn(node.id).filter((s) => keywords.has(s.label.toLowerCase()));
|
|
51
|
+
if (symbolMatches.length > 0) {
|
|
52
|
+
score += weights.symbolKeyword * Math.min(1, symbolMatches.length / 3);
|
|
53
|
+
reasons.push(`declares ${symbolMatches.length} matching symbol(s)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Penalty for generated/test files (unless intent justifies them).
|
|
57
|
+
if ((node.tags ?? []).includes('generated')) {
|
|
58
|
+
score -= weights.generatedPenalty;
|
|
59
|
+
reasons.push('generated (penalty)');
|
|
60
|
+
}
|
|
61
|
+
if ((node.tags ?? []).includes('test')) {
|
|
62
|
+
if (input.intent !== 'bug-fix') {
|
|
63
|
+
score -= weights.testPenalty;
|
|
64
|
+
reasons.push('test (intent-mismatched penalty)');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (score <= 0)
|
|
68
|
+
continue;
|
|
69
|
+
candidates.push({ node, score, reasons });
|
|
70
|
+
}
|
|
71
|
+
candidates.sort((a, b) => b.score - a.score || a.node.path.localeCompare(b.node.path));
|
|
72
|
+
return candidates;
|
|
73
|
+
}
|
|
74
|
+
function applyIntentTuning(intent) {
|
|
75
|
+
switch (intent) {
|
|
76
|
+
case 'bug-fix':
|
|
77
|
+
return { testFollowup: 0.6, testPenalty: -0.2 }; // negative penalty = boost
|
|
78
|
+
case 'refactor':
|
|
79
|
+
return { symbolKeyword: 0.9 };
|
|
80
|
+
case 'docs':
|
|
81
|
+
return { pathKeyword: 1.2 };
|
|
82
|
+
case 'release':
|
|
83
|
+
return { hintedPackage: 0.7 };
|
|
84
|
+
case 'migration':
|
|
85
|
+
return { symbolKeyword: 1.0, pathKeyword: 1.1 };
|
|
86
|
+
case 'feature':
|
|
87
|
+
case 'unknown':
|
|
88
|
+
default:
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function extractKeywords(task) {
|
|
93
|
+
const out = new Set();
|
|
94
|
+
const tokens = task.toLowerCase().split(/[^a-z0-9_$]+/).filter(Boolean);
|
|
95
|
+
for (const t of tokens) {
|
|
96
|
+
if (t.length < 3)
|
|
97
|
+
continue;
|
|
98
|
+
if (STOPWORDS.has(t))
|
|
99
|
+
continue;
|
|
100
|
+
out.add(t);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function countKeywordHits(text, keywords) {
|
|
105
|
+
const lower = text.toLowerCase();
|
|
106
|
+
let n = 0;
|
|
107
|
+
for (const k of keywords) {
|
|
108
|
+
if (lower.includes(k))
|
|
109
|
+
n += 1;
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact, deterministic context pack consumable by AI coding agents.
|
|
3
|
+
*
|
|
4
|
+
* Output of `@shrkcrft/context-planner`. Stable JSON shape so an agent
|
|
5
|
+
* can persist node ids across turns and verify what was cut.
|
|
6
|
+
*
|
|
7
|
+
* Schema: sharkcraft.context-pack/v1.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONTEXT_PACK_SCHEMA: "sharkcraft.context-pack/v1";
|
|
10
|
+
export type TaskIntent = 'feature' | 'bug-fix' | 'refactor' | 'docs' | 'release' | 'migration' | 'unknown';
|
|
11
|
+
export interface IRankedFile {
|
|
12
|
+
/** Project-relative POSIX path. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** Code-graph node id (`file:<path>`). */
|
|
15
|
+
nodeId: string;
|
|
16
|
+
/** Score in [0, 1]. Higher = more relevant. */
|
|
17
|
+
score: number;
|
|
18
|
+
/** Estimated token cost (approximate; deterministic). */
|
|
19
|
+
estimatedTokens: number;
|
|
20
|
+
/** Free-form reasons the file ranked. */
|
|
21
|
+
reasons: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export interface IRuleHit {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
severity?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface IPathHit {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ITemplateHit {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
export interface IRiskHit {
|
|
37
|
+
/** Risk category, e.g. 'cycle', 'public-api', 'cross-package'. */
|
|
38
|
+
kind: string;
|
|
39
|
+
/** Display label. */
|
|
40
|
+
label: string;
|
|
41
|
+
/** Related node ids (optional). */
|
|
42
|
+
refs?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export interface IContextPack {
|
|
45
|
+
schema: typeof CONTEXT_PACK_SCHEMA;
|
|
46
|
+
intent: TaskIntent;
|
|
47
|
+
/** Free-text task as provided by the caller. */
|
|
48
|
+
task: string;
|
|
49
|
+
/** Ranked relevant files (after token-budget pruning). */
|
|
50
|
+
files: readonly IRankedFile[];
|
|
51
|
+
/** Rule-graph hits over the selected file set. */
|
|
52
|
+
rules: readonly IRuleHit[];
|
|
53
|
+
paths: readonly IPathHit[];
|
|
54
|
+
templates: readonly ITemplateHit[];
|
|
55
|
+
/** Likely tests for the file set. */
|
|
56
|
+
tests: readonly string[];
|
|
57
|
+
/** Surfaced risks: cycles, public-API touches, etc. */
|
|
58
|
+
risks: readonly IRiskHit[];
|
|
59
|
+
/**
|
|
60
|
+
* Files the agent should NOT modify in this pack — generated files,
|
|
61
|
+
* vendored code, lock files, dist/build outputs.
|
|
62
|
+
*/
|
|
63
|
+
doNotTouch: readonly string[];
|
|
64
|
+
/** Token budget summary. */
|
|
65
|
+
budget: {
|
|
66
|
+
requested: number;
|
|
67
|
+
used: number;
|
|
68
|
+
/** True when the selection was capped by budget. */
|
|
69
|
+
truncated: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Free-form diagnostics. */
|
|
72
|
+
diagnostics: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=context-pack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-pack.d.ts","sourceRoot":"","sources":["../../src/schema/context-pack.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAG,4BAAqC,CAAC;AAEzE,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,SAAS,GACT,WAAW,GACX,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,eAAe,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,mBAAmB,CAAC;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,kDAAkD;IAClD,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;IACnC,qCAAqC;IACrC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,uDAAuD;IACvD,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B;;;OAGG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,4BAA4B;IAC5B,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,oDAAoD;QACpD,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,6BAA6B;IAC7B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact, deterministic context pack consumable by AI coding agents.
|
|
3
|
+
*
|
|
4
|
+
* Output of `@shrkcrft/context-planner`. Stable JSON shape so an agent
|
|
5
|
+
* can persist node ids across turns and verify what was cut.
|
|
6
|
+
*
|
|
7
|
+
* Schema: sharkcraft.context-pack/v1.
|
|
8
|
+
*/
|
|
9
|
+
export const CONTEXT_PACK_SCHEMA = 'sharkcraft.context-pack/v1';
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shrkcrft/context-planner",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"description": "SharkCraft context planner: deterministic, token-budgeted context packs for AI coding agents. Combines code graph, rule-graph, and intent classification to pick the minimal relevant file set for a task.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SharkCraft contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sharkcraft/sharkcraft.git",
|
|
25
|
+
"directory": "packages/context-planner"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/sharkcraft/sharkcraft",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sharkcraft/sharkcraft/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sharkcraft",
|
|
33
|
+
"context-planner",
|
|
34
|
+
"ai-agent",
|
|
35
|
+
"ranker"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"bun": ">=1.1.0",
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@shrkcrft/core": "^0.1.0-alpha.10",
|
|
46
|
+
"@shrkcrft/graph": "^0.1.0-alpha.10",
|
|
47
|
+
"@shrkcrft/rule-graph": "^0.1.0-alpha.10"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|