@mrciphersmith/keryx 0.2.80 → 0.2.81
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/cli.js +8058 -3466
- package/package.json +20 -6
- package/src/gdgraph/affected.test.ts +33 -0
- package/src/gdgraph/build.ts +54 -17
- package/src/gdgraph/fallback.test.ts +1 -1
- package/src/gdgraph/import-kind.test.ts +342 -22
- package/src/gdgraph/query.ts +14 -1
- package/src/gdgraph/service.test.ts +31 -1
- package/src/gdgraph/service.ts +33 -1
- package/src/gdgraph/staleness.test.ts +208 -0
- package/src/gdgraph/staleness.ts +195 -13
- package/src/gdgraph/symbols-capability.test.ts +98 -1
- package/src/gdgraph/symbols-capability.ts +105 -0
- package/src/gdgraph/treesitter/adapter.test.ts +104 -0
- package/src/gdgraph/treesitter/adapter.ts +146 -6
- package/src/gdgraph/treesitter/real-grammar-fixture.test.ts +71 -0
- package/src/gdgraph/types.ts +27 -2
- package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.codex.md +9 -6
- package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.cursor.md +9 -6
- package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.md +9 -6
- package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.opencode.md +9 -6
- package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.zed.md +9 -6
- package/src/gdskills/bundled/skills/orchestration/task-implementer/input-contract.schema.json +2 -2
- package/src/gdskills/bundled/skills/orchestration/task-implementer/orchestrator-prompt.md +2 -2
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
// `metaproject.json` — the opt-in switch for the symbol layer. Pure manifest
|
|
3
3
|
// transforms (the command layer owns file IO), merge-safe: preserves any other
|
|
4
4
|
// gdgraph capabilities and never rewrites unrelated modules.
|
|
5
|
+
//
|
|
6
|
+
// Also hosts `requireSymbols()` (AFC-13 / AC5, requirements 2 & 3): the typed,
|
|
7
|
+
// actionable error a caller gets when it explicitly demands symbol-level
|
|
8
|
+
// capability and the graph has no symbol layer — instead of `undefined`, an
|
|
9
|
+
// empty result, or a generic `Error` with a string. When given the tree-sitter
|
|
10
|
+
// adapter's per-language `GrammarDiagnosis[]` (see `./treesitter/adapter`),
|
|
11
|
+
// the error additionally distinguishes "grammar not installed" from "grammar
|
|
12
|
+
// installed but incompatible with this runtime" — distinct causes with
|
|
13
|
+
// distinct remedies, rather than one indistinguishable "unavailable".
|
|
14
|
+
|
|
15
|
+
import type { GrammarDiagnosis } from "./treesitter/adapter";
|
|
5
16
|
|
|
6
17
|
export const TREESITTER_CAPABILITY = "gdgraph.treesitter";
|
|
7
18
|
|
|
@@ -47,3 +58,97 @@ export function isTreesitterEnabled(manifest: Json): boolean {
|
|
|
47
58
|
(c) => c && typeof c === "object" && (c as Json).id === TREESITTER_CAPABILITY && (c as Json).enabled === true,
|
|
48
59
|
);
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
// --- requireSymbols (AFC-13 / AC5, requirements 2 & 3) ---
|
|
63
|
+
|
|
64
|
+
export type SymbolsUnavailableCode =
|
|
65
|
+
// The graph has no symbol layer and no diagnoses were supplied to explain
|
|
66
|
+
// why (capability disabled, or the caller didn't pass the adapter's probe).
|
|
67
|
+
| "no-symbol-layer"
|
|
68
|
+
// At least one required language's grammar never resolved: not installed,
|
|
69
|
+
// or failed the Asset Resolver's checksum.
|
|
70
|
+
| "grammar-missing"
|
|
71
|
+
// At least one required language's grammar resolved and verified on disk,
|
|
72
|
+
// but the installed `web-tree-sitter` runtime refused to load it (ABI /
|
|
73
|
+
// version mismatch) — distinct from "missing": nothing to install, the
|
|
74
|
+
// existing install doesn't match the runtime.
|
|
75
|
+
| "grammar-incompatible";
|
|
76
|
+
|
|
77
|
+
// Thrown by `requireSymbols()`. A typed, actionable error — never a generic
|
|
78
|
+
// `Error` with a string, never swallowed into `undefined`/an empty result —
|
|
79
|
+
// naming what capability is missing and what would fix it.
|
|
80
|
+
export class SymbolsUnavailableError extends Error {
|
|
81
|
+
readonly code: SymbolsUnavailableCode;
|
|
82
|
+
readonly remedy: string;
|
|
83
|
+
readonly diagnoses: GrammarDiagnosis[];
|
|
84
|
+
|
|
85
|
+
constructor(code: SymbolsUnavailableCode, message: string, remedy: string, diagnoses: GrammarDiagnosis[] = []) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "SymbolsUnavailableError";
|
|
88
|
+
this.code = code;
|
|
89
|
+
this.remedy = remedy;
|
|
90
|
+
this.diagnoses = diagnoses;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The minimal shape `requireSymbols` needs from a graph. Defined locally
|
|
95
|
+
// rather than imported from `./types` (owned by a concurrent task on this
|
|
96
|
+
// flow) so this module depends on nothing but the one field it actually
|
|
97
|
+
// reads; `GraphData` (which has `symbols?: SymbolNode[]`) satisfies this
|
|
98
|
+
// structurally, so callers can pass a real `GraphData` unchanged.
|
|
99
|
+
export interface SymbolsLayerCarrier {
|
|
100
|
+
symbols?: unknown[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Explicit demand for symbol-level capability: returns `graph.symbols` when
|
|
104
|
+
// present, otherwise THROWS `SymbolsUnavailableError` instead of returning
|
|
105
|
+
// `undefined` or an empty array. This is the "requireSymbols" contract in
|
|
106
|
+
// AC-13 — a caller that opts into requiring symbols gets a typed, actionable
|
|
107
|
+
// failure, never a silent downgrade to file-level.
|
|
108
|
+
//
|
|
109
|
+
// `diagnoses` (optional) is normally `TreesitterAdapter#getDiagnoses()`
|
|
110
|
+
// (`./treesitter/adapter`) forwarded by the caller; when supplied, the thrown
|
|
111
|
+
// error's `code` distinguishes "grammar-missing" from "grammar-incompatible"
|
|
112
|
+
// (requirement 3) instead of a single generic "unavailable".
|
|
113
|
+
export function requireSymbols<G extends SymbolsLayerCarrier>(
|
|
114
|
+
graph: G,
|
|
115
|
+
diagnoses: GrammarDiagnosis[] = [],
|
|
116
|
+
): NonNullable<G["symbols"]> {
|
|
117
|
+
if (graph.symbols !== undefined) {
|
|
118
|
+
return graph.symbols as NonNullable<G["symbols"]>;
|
|
119
|
+
}
|
|
120
|
+
throw buildSymbolsUnavailableError(diagnoses);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildSymbolsUnavailableError(diagnoses: GrammarDiagnosis[]): SymbolsUnavailableError {
|
|
124
|
+
const incompatible = diagnoses.filter((d) => d.status === "incompatible");
|
|
125
|
+
if (incompatible.length > 0) {
|
|
126
|
+
const languages = incompatible.map((d) => d.language).join(", ");
|
|
127
|
+
return new SymbolsUnavailableError(
|
|
128
|
+
"grammar-incompatible",
|
|
129
|
+
`requireSymbols: the installed grammar for [${languages}] is incompatible with this web-tree-sitter runtime (${incompatible
|
|
130
|
+
.map((d) => d.reason)
|
|
131
|
+
.join("; ")})`,
|
|
132
|
+
`Reinstall a "tree-sitter-<language>" grammar build that matches the installed web-tree-sitter runtime version (run "keryx gdgraph assets pull tree-sitter-<language>" after updating .metaproject/assets.lock.json), then rebuild the graph with "keryx gdgraph build".`,
|
|
133
|
+
diagnoses,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const missing = diagnoses.filter((d) => d.status === "missing");
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
const languages = missing.map((d) => d.language).join(", ");
|
|
140
|
+
return new SymbolsUnavailableError(
|
|
141
|
+
"grammar-missing",
|
|
142
|
+
`requireSymbols: no grammar is installed for [${languages}]`,
|
|
143
|
+
`Enable the "gdgraph.treesitter" capability in metaproject.json, install the grammar with "keryx gdgraph assets pull tree-sitter-<language>" for each of [${languages}], then rebuild the graph with "keryx gdgraph build".`,
|
|
144
|
+
diagnoses,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return new SymbolsUnavailableError(
|
|
149
|
+
"no-symbol-layer",
|
|
150
|
+
"requireSymbols: the graph has no symbol layer (gdgraph.treesitter is disabled, or the build ran without it)",
|
|
151
|
+
'Enable the "gdgraph.treesitter" capability in metaproject.json and rebuild the graph with "keryx gdgraph build".',
|
|
152
|
+
diagnoses,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -165,6 +165,110 @@ test("availability-false — missing dep ⇒ isAvailable false", async () => {
|
|
|
165
165
|
}
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
// --- AFC-13 / AC5 requirement 3: "the grammar is not installed" and "the
|
|
169
|
+
// grammar is installed but its ABI version does not match this runtime" must
|
|
170
|
+
// be distinguishable outcomes, not one collapsed "unavailable". Neither
|
|
171
|
+
// failure mode can be produced on a real machine without either uninstalling
|
|
172
|
+
// `web-tree-sitter` (not available as a seam) or installing an ABI-mismatched
|
|
173
|
+
// grammar build (forbidden by this task). Both are exercised instead through
|
|
174
|
+
// the module's own seam: `spec.load({ dep, asset })` accepts an injected
|
|
175
|
+
// `dep` exactly like the existing tests above, and a `Language.load` that
|
|
176
|
+
// rejects is a faithful simulation of a real ABI/version-mismatch error from
|
|
177
|
+
// `web-tree-sitter` (the real runtime rejects `Language.load` the same way
|
|
178
|
+
// for a grammar built against an incompatible ABI).
|
|
179
|
+
function brokenLanguageParserModule(rejection: Error): unknown {
|
|
180
|
+
function MockParser(this: unknown) {}
|
|
181
|
+
(MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
|
|
182
|
+
(MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
|
|
183
|
+
load: async () => {
|
|
184
|
+
throw rejection;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
MockParser.prototype.setLanguage = function setLanguage(): void {};
|
|
188
|
+
MockParser.prototype.parse = function parse(): { rootNode: TsNode } {
|
|
189
|
+
return { rootNode: bootTree() };
|
|
190
|
+
};
|
|
191
|
+
return MockParser;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
test("AC5.req3 — grammar asset never resolved ⇒ diagnosis status 'missing', not 'incompatible'", async () => {
|
|
195
|
+
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-missing-"));
|
|
196
|
+
try {
|
|
197
|
+
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
198
|
+
// No assets.lock.json entry at all ⇒ resolveGrammar can never find the asset.
|
|
199
|
+
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: null });
|
|
200
|
+
const adapter = spec.load({ dep: mockParserModule(), asset: null }) as unknown as {
|
|
201
|
+
isAvailable(): Promise<boolean>;
|
|
202
|
+
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(await adapter.isAvailable()).toBe(false);
|
|
206
|
+
const diagnoses = adapter.getDiagnoses();
|
|
207
|
+
expect(diagnoses).toHaveLength(1);
|
|
208
|
+
expect(diagnoses[0]?.status).toBe("missing");
|
|
209
|
+
expect(diagnoses[0]?.reason).not.toContain("ABI");
|
|
210
|
+
expect(diagnoses[0]?.reason).toContain("not resolved");
|
|
211
|
+
} finally {
|
|
212
|
+
await rm(root, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("AC5.req3 — grammar asset resolves and verifies, but the runtime refuses to load it ⇒ diagnosis status 'incompatible', not 'missing'", async () => {
|
|
217
|
+
const { root, grammarsDir } = await makeWorkspaceWithGrammar();
|
|
218
|
+
try {
|
|
219
|
+
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
|
|
220
|
+
const abiError = new Error("Incompatible language version 13. Expected minimum 14, maximum 15");
|
|
221
|
+
const adapter = spec.load({ dep: brokenLanguageParserModule(abiError), asset: null }) as unknown as {
|
|
222
|
+
isAvailable(): Promise<boolean>;
|
|
223
|
+
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// The grammar resolved (it is on disk with a verified checksum) — this is
|
|
227
|
+
// the crucial difference from the "missing" case above — but the runtime
|
|
228
|
+
// could not load it, so overall availability is still false.
|
|
229
|
+
expect(await adapter.isAvailable()).toBe(false);
|
|
230
|
+
const diagnoses = adapter.getDiagnoses();
|
|
231
|
+
expect(diagnoses).toHaveLength(1);
|
|
232
|
+
expect(diagnoses[0]?.status).toBe("incompatible");
|
|
233
|
+
expect(diagnoses[0]?.reason).toContain("Incompatible language version 13");
|
|
234
|
+
expect(diagnoses[0]?.reason).not.toContain("not resolved");
|
|
235
|
+
} finally {
|
|
236
|
+
await rm(root, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("AC5.req3 — missing and incompatible are genuinely different values end to end (adapter diagnosis -> warn reason -> requireSymbols code), never the same outcome", async () => {
|
|
241
|
+
const missingRoot = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-a-"));
|
|
242
|
+
const incompatibleWorkspace = await makeWorkspaceWithGrammar();
|
|
243
|
+
try {
|
|
244
|
+
await mkdir(path.join(missingRoot, ".metaproject"), { recursive: true });
|
|
245
|
+
const missingSpec = createTreesitterSpec(missingRoot, { languages: ["typescript"], grammarsPath: null });
|
|
246
|
+
const missingAdapter = missingSpec.load({ dep: mockParserModule(), asset: null }) as unknown as {
|
|
247
|
+
isAvailable(): Promise<boolean>;
|
|
248
|
+
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
249
|
+
};
|
|
250
|
+
await missingAdapter.isAvailable();
|
|
251
|
+
|
|
252
|
+
const incompatibleSpec = createTreesitterSpec(incompatibleWorkspace.root, {
|
|
253
|
+
languages: ["typescript"],
|
|
254
|
+
grammarsPath: incompatibleWorkspace.grammarsDir,
|
|
255
|
+
});
|
|
256
|
+
const incompatibleAdapter = incompatibleSpec.load({
|
|
257
|
+
dep: brokenLanguageParserModule(new Error("bad wasm magic number")),
|
|
258
|
+
asset: null,
|
|
259
|
+
}) as unknown as {
|
|
260
|
+
isAvailable(): Promise<boolean>;
|
|
261
|
+
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
262
|
+
};
|
|
263
|
+
await incompatibleAdapter.isAvailable();
|
|
264
|
+
|
|
265
|
+
expect(missingAdapter.getDiagnoses()[0]?.status).not.toBe(incompatibleAdapter.getDiagnoses()[0]?.status);
|
|
266
|
+
} finally {
|
|
267
|
+
await rm(missingRoot, { recursive: true, force: true });
|
|
268
|
+
await rm(incompatibleWorkspace.root, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
168
272
|
test("AC1.1 additive write path — enrich writes symbols.jsonl + calls.jsonl via a mock adapter", async () => {
|
|
169
273
|
const root = await mkdtemp(path.join(tmpdir(), "keryx-enrich-"));
|
|
170
274
|
try {
|
|
@@ -37,6 +37,46 @@ export interface TreesitterAdapterConfig {
|
|
|
37
37
|
grammarsPath: string | null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Per-language probe outcome (AFC-13 / AC5). "missing" and "incompatible" are
|
|
41
|
+
// deliberately distinct: a grammar that never resolved (not installed, or
|
|
42
|
+
// failed the Asset Resolver's checksum) has a different remedy — install it —
|
|
43
|
+
// from one that resolved and verified on disk but the installed
|
|
44
|
+
// `web-tree-sitter` runtime refuses to load it (an ABI/version mismatch
|
|
45
|
+
// between the pinned grammar build and the runtime) — whose remedy is
|
|
46
|
+
// reinstalling a matching build, not installing anything new. Collapsing
|
|
47
|
+
// these into one "unavailable" outcome is the defect class AC5 targets.
|
|
48
|
+
export type GrammarDiagnosisStatus = "ok" | "missing" | "incompatible";
|
|
49
|
+
|
|
50
|
+
export interface GrammarDiagnosis {
|
|
51
|
+
language: GrammarLanguage;
|
|
52
|
+
status: GrammarDiagnosisStatus;
|
|
53
|
+
// Human-actionable detail; empty for "ok".
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Render diagnoses into one warn-once-friendly summary line, grouped by
|
|
58
|
+
// status so a mixed missing+incompatible result stays legible.
|
|
59
|
+
export function describeGrammarDiagnoses(diagnoses: GrammarDiagnosis[]): string {
|
|
60
|
+
if (diagnoses.length === 0) {
|
|
61
|
+
return "no grammar languages configured";
|
|
62
|
+
}
|
|
63
|
+
const byStatus = (status: GrammarDiagnosisStatus) =>
|
|
64
|
+
diagnoses.filter((d) => d.status === status).map((d) => d.language);
|
|
65
|
+
const missing = byStatus("missing");
|
|
66
|
+
const incompatible = byStatus("incompatible");
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
parts.push(`missing grammar for [${missing.join(", ")}]`);
|
|
70
|
+
}
|
|
71
|
+
if (incompatible.length > 0) {
|
|
72
|
+
parts.push(`incompatible grammar for [${incompatible.join(", ")}]`);
|
|
73
|
+
}
|
|
74
|
+
if (parts.length === 0) {
|
|
75
|
+
return "no configured grammar resolved to a usable parser";
|
|
76
|
+
}
|
|
77
|
+
return parts.join("; ");
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
// Minimal shapes of the `web-tree-sitter` surface we touch (kept local so the
|
|
41
81
|
// dep is never imported for types either — structural typing only).
|
|
42
82
|
interface ParserLike {
|
|
@@ -138,7 +178,15 @@ export async function resolveTreesitterCapability(
|
|
|
138
178
|
return null;
|
|
139
179
|
}
|
|
140
180
|
if (!available) {
|
|
141
|
-
|
|
181
|
+
// Prefer the per-language diagnoses when the concrete adapter exposes
|
|
182
|
+
// them (only `TreesitterAdapter` does — this cast is local and narrow,
|
|
183
|
+
// never leaks into the shared `CapabilityAdapter` interface), so the
|
|
184
|
+
// one-line warn-once already names missing vs incompatible instead of
|
|
185
|
+
// a generic "unavailable".
|
|
186
|
+
const diagnoses = getGrammarDiagnoses(adapter as unknown as { getDiagnoses?: () => GrammarDiagnosis[] });
|
|
187
|
+
const reason =
|
|
188
|
+
diagnoses.length > 0 ? describeGrammarDiagnoses(diagnoses) : "adapter reported unavailable";
|
|
189
|
+
warnCapabilityDegraded(spec.id, reason);
|
|
142
190
|
return null;
|
|
143
191
|
}
|
|
144
192
|
|
|
@@ -149,9 +197,23 @@ export async function resolveTreesitterCapability(
|
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
|
|
200
|
+
// Read `getDiagnoses()` off a `CapabilityAdapter` when the concrete instance
|
|
201
|
+
// is a `TreesitterAdapter` (the only implementation today). A generic
|
|
202
|
+
// `CapabilityAdapter` from another module simply has no such method, so this
|
|
203
|
+
// is a safe narrow probe rather than an assumption about the seam's shared
|
|
204
|
+
// interface.
|
|
205
|
+
function getGrammarDiagnoses(adapter: { getDiagnoses?: () => GrammarDiagnosis[] }): GrammarDiagnosis[] {
|
|
206
|
+
return typeof adapter.getDiagnoses === "function" ? adapter.getDiagnoses() : [];
|
|
207
|
+
}
|
|
208
|
+
|
|
152
209
|
class TreesitterAdapter implements CapabilityAdapter<BuildInput, SymbolLayer> {
|
|
153
210
|
readonly id = "gdgraph.treesitter";
|
|
154
211
|
private grammars: ResolvedGrammar[] = [];
|
|
212
|
+
private diagnoses: GrammarDiagnosis[] = [];
|
|
213
|
+
// Populated by `isAvailable()`'s probe so `run()` never re-attempts a
|
|
214
|
+
// `loadLanguage()` call the probe already made (and, for a language that
|
|
215
|
+
// probed "incompatible", never attempts it at all).
|
|
216
|
+
private loadedLanguages = new Map<GrammarLanguage, unknown>();
|
|
155
217
|
|
|
156
218
|
constructor(
|
|
157
219
|
private readonly cwd: string,
|
|
@@ -159,13 +221,88 @@ class TreesitterAdapter implements CapabilityAdapter<BuildInput, SymbolLayer> {
|
|
|
159
221
|
private readonly dep: unknown,
|
|
160
222
|
) {}
|
|
161
223
|
|
|
224
|
+
// Exposes the per-language missing-vs-incompatible breakdown from the most
|
|
225
|
+
// recent `isAvailable()` probe (AFC-13 / AC5, requirement 3). Empty before
|
|
226
|
+
// `isAvailable()` has run.
|
|
227
|
+
getDiagnoses(): GrammarDiagnosis[] {
|
|
228
|
+
return this.diagnoses;
|
|
229
|
+
}
|
|
230
|
+
|
|
162
231
|
async isAvailable(): Promise<boolean> {
|
|
232
|
+
const languages = toGrammarLanguages(this.config.languages);
|
|
233
|
+
this.diagnoses = [];
|
|
234
|
+
this.loadedLanguages = new Map();
|
|
235
|
+
this.grammars = [];
|
|
236
|
+
|
|
163
237
|
if (!this.dep) {
|
|
238
|
+
this.diagnoses = languages.map((language) => ({
|
|
239
|
+
language,
|
|
240
|
+
status: "missing",
|
|
241
|
+
reason: 'optional dependency "web-tree-sitter" is not installed',
|
|
242
|
+
}));
|
|
164
243
|
return false;
|
|
165
244
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
|
|
246
|
+
const resolved = await resolveGrammars(this.cwd, languages, this.config.grammarsPath);
|
|
247
|
+
const resolvedByLanguage = new Map(resolved.map((grammar) => [grammar.language, grammar]));
|
|
248
|
+
|
|
249
|
+
const api = normalizeParserApi(this.dep);
|
|
250
|
+
if (typeof api?.init === "function") {
|
|
251
|
+
try {
|
|
252
|
+
await api.init();
|
|
253
|
+
} catch {
|
|
254
|
+
// Runtime init failure ⇒ every resolved grammar is unloadable; each
|
|
255
|
+
// still gets its own diagnosis below (loadLanguage will also throw).
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const language of languages) {
|
|
260
|
+
const grammar = resolvedByLanguage.get(language);
|
|
261
|
+
if (!grammar) {
|
|
262
|
+
this.diagnoses.push({
|
|
263
|
+
language,
|
|
264
|
+
status: "missing",
|
|
265
|
+
reason: `grammar asset "tree-sitter-${language}" is not resolved (not installed, or failed checksum verification)`,
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!api) {
|
|
270
|
+
this.diagnoses.push({
|
|
271
|
+
language,
|
|
272
|
+
status: "incompatible",
|
|
273
|
+
reason: 'the "web-tree-sitter" module shape was not recognized (no usable Parser export)',
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const loaded = await api.loadLanguage(grammar.path);
|
|
279
|
+
if (!loaded) {
|
|
280
|
+
this.diagnoses.push({
|
|
281
|
+
language,
|
|
282
|
+
status: "incompatible",
|
|
283
|
+
reason: `grammar at "${grammar.path}" loaded but produced no Language object`,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
this.loadedLanguages.set(language, loaded);
|
|
288
|
+
this.grammars.push(grammar);
|
|
289
|
+
this.diagnoses.push({ language, status: "ok", reason: "" });
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// The grammar resolved and verified on disk, but the installed
|
|
292
|
+
// `web-tree-sitter` runtime refused to load it — an ABI/version
|
|
293
|
+
// mismatch between the pinned grammar build and the runtime, NOT a
|
|
294
|
+
// missing asset. Distinct cause, distinct remedy (AFC-13).
|
|
295
|
+
this.diagnoses.push({
|
|
296
|
+
language,
|
|
297
|
+
status: "incompatible",
|
|
298
|
+
reason: `grammar at "${grammar.path}" failed to load in the installed web-tree-sitter runtime (likely an ABI/version mismatch): ${
|
|
299
|
+
error instanceof Error ? error.message : String(error)
|
|
300
|
+
}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.diagnoses.some((d) => d.status === "ok");
|
|
169
306
|
}
|
|
170
307
|
|
|
171
308
|
async run(input: BuildInput): Promise<SymbolLayer> {
|
|
@@ -178,11 +315,14 @@ class TreesitterAdapter implements CapabilityAdapter<BuildInput, SymbolLayer> {
|
|
|
178
315
|
await api.init();
|
|
179
316
|
}
|
|
180
317
|
|
|
181
|
-
// Load + cache one parser per resolved grammar language.
|
|
318
|
+
// Load + cache one parser per resolved grammar language. Reuse the
|
|
319
|
+
// Language object the `isAvailable()` probe already loaded when present
|
|
320
|
+
// (the common path); fall back to loading directly for a caller that
|
|
321
|
+
// invokes `run()` without having called `isAvailable()` first.
|
|
182
322
|
const parsers = new Map<GrammarLanguage, ParserLike>();
|
|
183
323
|
for (const grammar of this.grammars) {
|
|
184
324
|
try {
|
|
185
|
-
const language = await api.loadLanguage(grammar.path);
|
|
325
|
+
const language = this.loadedLanguages.get(grammar.language) ?? (await api.loadLanguage(grammar.path));
|
|
186
326
|
if (!language) {
|
|
187
327
|
continue;
|
|
188
328
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// AFC-13 / AC5 requirement 4: "fixture даёт реальные symbols после явной
|
|
2
|
+
// установки" — a fixture that genuinely yields real symbols after an
|
|
3
|
+
// explicit runtime/grammar installation. This task is forbidden from
|
|
4
|
+
// performing that installation (no dependency installs of any kind).
|
|
5
|
+
//
|
|
6
|
+
// So this file does NOT install anything. It probes whether a working
|
|
7
|
+
// `web-tree-sitter` runtime + a real, verified grammar are ALREADY present in
|
|
8
|
+
// this environment (exactly the state an explicit install would produce) and:
|
|
9
|
+
// - genuinely present ⇒ the test RUNS FOR REAL against the real adapter
|
|
10
|
+
// (`resolveTreesitterCapability`, the actual production entry point —
|
|
11
|
+
// not a mock) and asserts real, non-empty symbols extracted from a real
|
|
12
|
+
// TypeScript fixture by the real grammar.
|
|
13
|
+
// - genuinely absent ⇒ `test.skipIf` marks the test SKIPPED (visible as
|
|
14
|
+
// "skip" in `bun test`'s own summary, not a silent pass), with the reason
|
|
15
|
+
// embedded in the test name.
|
|
16
|
+
//
|
|
17
|
+
// A vacuous pass (the "absent" branch quietly returning without asserting
|
|
18
|
+
// anything) would NOT be evidence for requirement 4 — `test.skipIf` is used
|
|
19
|
+
// specifically so an absent environment shows up as a skip, never as a pass.
|
|
20
|
+
|
|
21
|
+
import { expect, test } from "bun:test";
|
|
22
|
+
import { resolveTreesitterCapability } from "./adapter";
|
|
23
|
+
|
|
24
|
+
const FIXTURE_SOURCE = ["export function boot(): void {", " tick();", "}", "", "function tick(): void {}", ""].join(
|
|
25
|
+
"\n",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Probed once at module load (top-level await — this file's only test needs
|
|
29
|
+
// the answer before `test.skipIf` decides whether to run). This calls the
|
|
30
|
+
// REAL `resolveTreesitterCapability` against the real project root, so it
|
|
31
|
+
// reflects whatever runtime + grammar state is genuinely on this machine —
|
|
32
|
+
// no mocking, no injected `dep`.
|
|
33
|
+
const projectRoot = process.cwd();
|
|
34
|
+
const probedAdapter = await resolveTreesitterCapability(projectRoot, {
|
|
35
|
+
languages: ["typescript"],
|
|
36
|
+
grammarsPath: null,
|
|
37
|
+
});
|
|
38
|
+
const skipReason = probedAdapter
|
|
39
|
+
? ""
|
|
40
|
+
: "no working web-tree-sitter runtime + verified typescript grammar resolved in this environment " +
|
|
41
|
+
"(gdgraph.treesitter must be enabled in metaproject.json AND a matching grammar installed via " +
|
|
42
|
+
'"keryx gdgraph assets pull tree-sitter-typescript" — an install this task must not perform)';
|
|
43
|
+
|
|
44
|
+
test.skipIf(!probedAdapter)(
|
|
45
|
+
skipReason ? `AC5.req4 — real fixture yields real symbols [SKIPPED: ${skipReason}]` : "AC5.req4 — real fixture yields real symbols after explicit installation",
|
|
46
|
+
async () => {
|
|
47
|
+
const adapter = probedAdapter;
|
|
48
|
+
if (!adapter) {
|
|
49
|
+
// Unreachable when the test actually runs (skipIf gates it), kept only
|
|
50
|
+
// so TypeScript sees the non-null adapter below without a cast.
|
|
51
|
+
throw new Error("AC5.req4 ran without a resolved adapter — skipIf condition was wrong");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const layer = await adapter.run({
|
|
55
|
+
files: [{ path: "fixtures/treesitter/ac5-req4-fixture.ts", content: FIXTURE_SOURCE }],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Real symbols, not a mock's canned output: names, kinds, and a resolved
|
|
59
|
+
// call edge all come from the actually-installed tree-sitter grammar
|
|
60
|
+
// parsing actual TypeScript source.
|
|
61
|
+
expect(layer.symbols.length).toBeGreaterThan(0);
|
|
62
|
+
const byName = Object.fromEntries(layer.symbols.map((s) => [s.name, s]));
|
|
63
|
+
expect(byName.boot).toMatchObject({ kind: "function", language: "typescript" });
|
|
64
|
+
expect(byName.tick).toMatchObject({ kind: "function", language: "typescript" });
|
|
65
|
+
|
|
66
|
+
const callSummaries = layer.calls.map((c) => `${c.kind}:${c.from}=>${c.to}`);
|
|
67
|
+
expect(callSummaries).toContain(
|
|
68
|
+
"calls:fixtures/treesitter/ac5-req4-fixture.ts#boot=>fixtures/treesitter/ac5-req4-fixture.ts#tick",
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
);
|
package/src/gdgraph/types.ts
CHANGED
|
@@ -27,10 +27,35 @@ export type TranspilerImportKind =
|
|
|
27
27
|
// plain regex with no notion of "static" vs "dynamic". Never infer one;
|
|
28
28
|
// `UNKNOWN_IMPORT_KIND` marks it explicitly and cycle detection treats it as
|
|
29
29
|
// load-order (the pre-fix behavior), so fallback-only edges are never
|
|
30
|
-
// silently excluded from a real cycle.
|
|
30
|
+
// silently excluded from a real cycle. This is reserved for the case where
|
|
31
|
+
// `scanImports` could not even run (unparseable source) or the language has
|
|
32
|
+
// no transpiler support at all (Java/Python) — genuinely unknown provenance,
|
|
33
|
+
// where "assume load-order" is the safe default.
|
|
31
34
|
export const UNKNOWN_IMPORT_KIND = "unknown-static" as const;
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
// AFC-11 (flow 234): a specifier the transpiler DID successfully scan the
|
|
37
|
+
// file for, but did not report for THIS statement, because the statement
|
|
38
|
+
// carries no runtime binding — `import type {...}`, `export type {...} from`,
|
|
39
|
+
// or an inline specifier list where every name is `type`-prefixed
|
|
40
|
+
// (`import { type X } from "./m"`). `Bun.Transpiler#scanImports` erases these
|
|
41
|
+
// (verified empirically: this repo's `tsconfig.json` sets neither
|
|
42
|
+
// `importsNotUsedAsValues` nor `verbatimModuleSyntax`, so it runs on
|
|
43
|
+
// TypeScript's defaults, which elide type-only imports/exports at compile
|
|
44
|
+
// time), so they only reach the graph through the regex fallback below.
|
|
45
|
+
// Unlike `UNKNOWN_IMPORT_KIND`, the provenance here is NOT unknown — it is
|
|
46
|
+
// known to be type-only — so it gets its own kind rather than being folded
|
|
47
|
+
// into "unknown-static": `getCycles` (query.ts) excludes it from the
|
|
48
|
+
// load-order adjacency (a type-only cycle cannot deadlock a runtime module
|
|
49
|
+
// graph — it does not exist once compiled away), while every consumer that
|
|
50
|
+
// reads `edge.kind` (getOrphans, getAffected, computeAffected) is untouched
|
|
51
|
+
// and keeps treating it as a real dependency edge for impact analysis, since
|
|
52
|
+
// the edge is genuinely real for "what has to be re-checked".
|
|
53
|
+
export const TYPE_ONLY_IMPORT_KIND = "type-only" as const;
|
|
54
|
+
|
|
55
|
+
export type ImportKind =
|
|
56
|
+
| TranspilerImportKind
|
|
57
|
+
| typeof UNKNOWN_IMPORT_KIND
|
|
58
|
+
| typeof TYPE_ONLY_IMPORT_KIND;
|
|
34
59
|
|
|
35
60
|
export type GraphEdge = {
|
|
36
61
|
id: string;
|
|
@@ -10,7 +10,7 @@ triggers:
|
|
|
10
10
|
- "Implement issue task"
|
|
11
11
|
metadata:
|
|
12
12
|
author: "MrCipherSmith"
|
|
13
|
-
version: "1.3.
|
|
13
|
+
version: "1.3.1"
|
|
14
14
|
category: "implementation"
|
|
15
15
|
agent_worthy: true
|
|
16
16
|
compatible_harnesses: "claude,cursor,codex,zed,opencode"
|
|
@@ -23,7 +23,7 @@ license: "MIT"
|
|
|
23
23
|
|
|
24
24
|
Receives a single atomic task (JSON task object from `issue-analyzer`) and implements it end-to-end. Designed to run autonomously as a sub-agent — no user interaction required. Commits its changes to a shared feature branch managed by the orchestrator.
|
|
25
25
|
|
|
26
|
-
**Input:** JSON task object + workspace context (branch, codebase path, issue number)
|
|
26
|
+
**Input:** JSON task object + workspace context (branch, codebase path, optional real issue number)
|
|
27
27
|
**Output:** JSON result object with implementation status, files modified, verification results
|
|
28
28
|
|
|
29
29
|
## When to Use
|
|
@@ -86,10 +86,12 @@ TASK: (from JSON object passed by orchestrator)
|
|
|
86
86
|
WORKSPACE:
|
|
87
87
|
codebase_path: absolute path to the repository
|
|
88
88
|
branch: feature branch to work on (already checked out by orchestrator)
|
|
89
|
-
issue_number: GitHub issue number
|
|
89
|
+
issue_number: Optional real GitHub issue number; omit for description-based tasks
|
|
90
90
|
issue_title: issue title (for commit messages)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
For a description-based task, omit `issue_number` and issue references in commit messages. Never substitute a flow ID or fabricate a GitHub issue. A supplied issue number must remain a positive integer.
|
|
94
|
+
|
|
93
95
|
**1.3 For fix tasks (dispatched from review loop):**
|
|
94
96
|
|
|
95
97
|
```
|
|
@@ -120,7 +122,7 @@ The refusals are the contract's, not a checklist you run by eye
|
|
|
120
122
|
| `task_type` outside `ui_component\|store_logic\|service_api\|refactoring\|fix\|mixed` | `task.task_type.enum` |
|
|
121
123
|
| A `task_id` that is not `task-<n>` | `task.task_id.pattern` |
|
|
122
124
|
| Empty `target_files` or empty `acceptance_criteria` | `minItems: 1` on both |
|
|
123
|
-
| Missing `codebase_path` / `branch`
|
|
125
|
+
| Missing `codebase_path` / `branch` | `workspace.required` |
|
|
124
126
|
| `skip_confirmation` anything but `true` | `automation.skip_confirmation.const` |
|
|
125
127
|
| `max_self_fix_attempts` above 3 | `automation.max_self_fix_attempts.maximum` |
|
|
126
128
|
| Any field the contract does not declare | `additionalProperties: false` |
|
|
@@ -315,7 +317,7 @@ Execute the change plan. Write production-quality code.
|
|
|
315
317
|
|
|
316
318
|
**4.5 Commit after implementation:**
|
|
317
319
|
|
|
318
|
-
|
|
320
|
+
When auto-commit is enabled, create a conventional commit with the changes. Include the `refs #<issue_number>` line below only when a positive real issue number was supplied; otherwise omit that entire line:
|
|
319
321
|
```bash
|
|
320
322
|
git add <modified files>
|
|
321
323
|
git commit -m "<type>(<scope>): <description>
|
|
@@ -399,6 +401,7 @@ said. Report the block instead, naming what repeated.
|
|
|
399
401
|
**Never run `git reset --hard`, `git clean`, or any unscoped revert.** You do not own the worktree. `job-orchestrator` dispatches implementers in PARALLEL WAVES sharing a single worktree, so an unscoped reset destroys a wave-mate's uncommitted work — work that is not yours, cannot be recovered, and whose loss is invisible to you because the other agent's failure surfaces somewhere else entirely. If you cannot identify which files are yours, leave the tree exactly as it is and say so in the report: a dirty tree is recoverable, a destroyed one is not.
|
|
400
402
|
|
|
401
403
|
**5.5 Re-commit fixes if any:**
|
|
404
|
+
When auto-commit is enabled, use the template below. Include `refs #<issue_number>` only for a supplied positive real issue number; otherwise omit that entire line.
|
|
402
405
|
```bash
|
|
403
406
|
git add <fixed files>
|
|
404
407
|
git commit -m "fix(<scope>): resolve lint/type/test issues
|
|
@@ -530,7 +533,7 @@ second copy of a schema is how that happens.
|
|
|
530
533
|
5. **DO** use project path aliases (`@components`, `@utils`, etc.) for imports.
|
|
531
534
|
6. **DO** wrap React components with `observer()` when they access MobX stores.
|
|
532
535
|
7. **DO** use `runInAction()` after every `await` in MobX actions.
|
|
533
|
-
8. **DO**
|
|
536
|
+
8. **DO** use conventional commit format when auto-commit is enabled. Reference only a supplied real issue number; omit the issue reference when absent.
|
|
534
537
|
9. **DO** verify your work before reporting.
|
|
535
538
|
10. **DO** make `STATUS: <TOKEN>` the first line of your final message, and put no
|
|
536
539
|
JSON in the response body. The full JSON result is the file Phase 6.1 writes
|
|
@@ -10,7 +10,7 @@ triggers:
|
|
|
10
10
|
- "Implement issue task"
|
|
11
11
|
metadata:
|
|
12
12
|
author: "MrCipherSmith"
|
|
13
|
-
version: "1.3.
|
|
13
|
+
version: "1.3.1"
|
|
14
14
|
category: "implementation"
|
|
15
15
|
agent_worthy: true
|
|
16
16
|
compatible_harnesses: "claude,cursor,codex,zed,opencode"
|
|
@@ -23,7 +23,7 @@ license: "MIT"
|
|
|
23
23
|
|
|
24
24
|
Receives a single atomic task (JSON task object from `issue-analyzer`) and implements it end-to-end. Designed to run autonomously as a sub-agent — no user interaction required. Commits its changes to a shared feature branch managed by the orchestrator.
|
|
25
25
|
|
|
26
|
-
**Input:** JSON task object + workspace context (branch, codebase path, issue number)
|
|
26
|
+
**Input:** JSON task object + workspace context (branch, codebase path, optional real issue number)
|
|
27
27
|
**Output:** JSON result object with implementation status, files modified, verification results
|
|
28
28
|
|
|
29
29
|
## When to Use
|
|
@@ -86,10 +86,12 @@ TASK: (from JSON object passed by orchestrator)
|
|
|
86
86
|
WORKSPACE:
|
|
87
87
|
codebase_path: absolute path to the repository
|
|
88
88
|
branch: feature branch to work on (already checked out by orchestrator)
|
|
89
|
-
issue_number: GitHub issue number
|
|
89
|
+
issue_number: Optional real GitHub issue number; omit for description-based tasks
|
|
90
90
|
issue_title: issue title (for commit messages)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
For a description-based task, omit `issue_number` and issue references in commit messages. Never substitute a flow ID or fabricate a GitHub issue. A supplied issue number must remain a positive integer.
|
|
94
|
+
|
|
93
95
|
**1.3 For fix tasks (dispatched from review loop):**
|
|
94
96
|
|
|
95
97
|
```
|
|
@@ -120,7 +122,7 @@ The refusals are the contract's, not a checklist you run by eye
|
|
|
120
122
|
| `task_type` outside `ui_component\|store_logic\|service_api\|refactoring\|fix\|mixed` | `task.task_type.enum` |
|
|
121
123
|
| A `task_id` that is not `task-<n>` | `task.task_id.pattern` |
|
|
122
124
|
| Empty `target_files` or empty `acceptance_criteria` | `minItems: 1` on both |
|
|
123
|
-
| Missing `codebase_path` / `branch`
|
|
125
|
+
| Missing `codebase_path` / `branch` | `workspace.required` |
|
|
124
126
|
| `skip_confirmation` anything but `true` | `automation.skip_confirmation.const` |
|
|
125
127
|
| `max_self_fix_attempts` above 3 | `automation.max_self_fix_attempts.maximum` |
|
|
126
128
|
| Any field the contract does not declare | `additionalProperties: false` |
|
|
@@ -315,7 +317,7 @@ Execute the change plan. Write production-quality code.
|
|
|
315
317
|
|
|
316
318
|
**4.5 Commit after implementation:**
|
|
317
319
|
|
|
318
|
-
|
|
320
|
+
When auto-commit is enabled, create a conventional commit with the changes. Include the `refs #<issue_number>` line below only when a positive real issue number was supplied; otherwise omit that entire line:
|
|
319
321
|
```bash
|
|
320
322
|
git add <modified files>
|
|
321
323
|
git commit -m "<type>(<scope>): <description>
|
|
@@ -399,6 +401,7 @@ said. Report the block instead, naming what repeated.
|
|
|
399
401
|
**Never run `git reset --hard`, `git clean`, or any unscoped revert.** You do not own the worktree. `job-orchestrator` dispatches implementers in PARALLEL WAVES sharing a single worktree, so an unscoped reset destroys a wave-mate's uncommitted work — work that is not yours, cannot be recovered, and whose loss is invisible to you because the other agent's failure surfaces somewhere else entirely. If you cannot identify which files are yours, leave the tree exactly as it is and say so in the report: a dirty tree is recoverable, a destroyed one is not.
|
|
400
402
|
|
|
401
403
|
**5.5 Re-commit fixes if any:**
|
|
404
|
+
When auto-commit is enabled, use the template below. Include `refs #<issue_number>` only for a supplied positive real issue number; otherwise omit that entire line.
|
|
402
405
|
```bash
|
|
403
406
|
git add <fixed files>
|
|
404
407
|
git commit -m "fix(<scope>): resolve lint/type/test issues
|
|
@@ -530,7 +533,7 @@ second copy of a schema is how that happens.
|
|
|
530
533
|
5. **DO** use project path aliases (`@components`, `@utils`, etc.) for imports.
|
|
531
534
|
6. **DO** wrap React components with `observer()` when they access MobX stores.
|
|
532
535
|
7. **DO** use `runInAction()` after every `await` in MobX actions.
|
|
533
|
-
8. **DO**
|
|
536
|
+
8. **DO** use conventional commit format when auto-commit is enabled. Reference only a supplied real issue number; omit the issue reference when absent.
|
|
534
537
|
9. **DO** verify your work before reporting.
|
|
535
538
|
10. **DO** make `STATUS: <TOKEN>` the first line of your final message, and put no
|
|
536
539
|
JSON in the response body. The full JSON result is the file Phase 6.1 writes
|