@descryy/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/dist/action-handshake.d.ts +86 -0
  3. package/dist/action-handshake.d.ts.map +1 -0
  4. package/dist/action-handshake.js +144 -0
  5. package/dist/action-handshake.js.map +1 -0
  6. package/dist/bin/descry-mcp.d.ts +18 -0
  7. package/dist/bin/descry-mcp.d.ts.map +1 -0
  8. package/dist/bin/descry-mcp.js +54 -0
  9. package/dist/bin/descry-mcp.js.map +1 -0
  10. package/dist/index.d.ts +32 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +15 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/language-guess.d.ts +17 -0
  15. package/dist/language-guess.d.ts.map +1 -0
  16. package/dist/language-guess.js +43 -0
  17. package/dist/language-guess.js.map +1 -0
  18. package/dist/protocol.d.ts +91 -0
  19. package/dist/protocol.d.ts.map +1 -0
  20. package/dist/protocol.js +127 -0
  21. package/dist/protocol.js.map +1 -0
  22. package/dist/registry.d.ts +72 -0
  23. package/dist/registry.d.ts.map +1 -0
  24. package/dist/registry.js +136 -0
  25. package/dist/registry.js.map +1 -0
  26. package/dist/render.d.ts +275 -0
  27. package/dist/render.d.ts.map +1 -0
  28. package/dist/render.js +333 -0
  29. package/dist/render.js.map +1 -0
  30. package/dist/server.d.ts +42 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +263 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/session.d.ts +164 -0
  35. package/dist/session.d.ts.map +1 -0
  36. package/dist/session.js +302 -0
  37. package/dist/session.js.map +1 -0
  38. package/dist/tools/analyze.d.ts +124 -0
  39. package/dist/tools/analyze.d.ts.map +1 -0
  40. package/dist/tools/analyze.js +411 -0
  41. package/dist/tools/analyze.js.map +1 -0
  42. package/dist/tools/contracts.d.ts +105 -0
  43. package/dist/tools/contracts.d.ts.map +1 -0
  44. package/dist/tools/contracts.js +141 -0
  45. package/dist/tools/contracts.js.map +1 -0
  46. package/dist/tools/cross-pr.d.ts +137 -0
  47. package/dist/tools/cross-pr.d.ts.map +1 -0
  48. package/dist/tools/cross-pr.js +216 -0
  49. package/dist/tools/cross-pr.js.map +1 -0
  50. package/dist/tools/git-diff.d.ts +29 -0
  51. package/dist/tools/git-diff.d.ts.map +1 -0
  52. package/dist/tools/git-diff.js +117 -0
  53. package/dist/tools/git-diff.js.map +1 -0
  54. package/dist/tools/git-history.d.ts +27 -0
  55. package/dist/tools/git-history.d.ts.map +1 -0
  56. package/dist/tools/git-history.js +91 -0
  57. package/dist/tools/git-history.js.map +1 -0
  58. package/dist/tools/history.d.ts +63 -0
  59. package/dist/tools/history.d.ts.map +1 -0
  60. package/dist/tools/history.js +114 -0
  61. package/dist/tools/history.js.map +1 -0
  62. package/dist/tools/impact.d.ts +81 -0
  63. package/dist/tools/impact.d.ts.map +1 -0
  64. package/dist/tools/impact.js +229 -0
  65. package/dist/tools/impact.js.map +1 -0
  66. package/dist/tools/index.d.ts +33 -0
  67. package/dist/tools/index.d.ts.map +1 -0
  68. package/dist/tools/index.js +47 -0
  69. package/dist/tools/index.js.map +1 -0
  70. package/dist/tools/kit.d.ts +183 -0
  71. package/dist/tools/kit.d.ts.map +1 -0
  72. package/dist/tools/kit.js +117 -0
  73. package/dist/tools/kit.js.map +1 -0
  74. package/dist/tools/link-workspace.d.ts +62 -0
  75. package/dist/tools/link-workspace.d.ts.map +1 -0
  76. package/dist/tools/link-workspace.js +156 -0
  77. package/dist/tools/link-workspace.js.map +1 -0
  78. package/dist/tools/lookup.d.ts +29 -0
  79. package/dist/tools/lookup.d.ts.map +1 -0
  80. package/dist/tools/lookup.js +73 -0
  81. package/dist/tools/lookup.js.map +1 -0
  82. package/dist/tools/pr-analysis.d.ts +89 -0
  83. package/dist/tools/pr-analysis.d.ts.map +1 -0
  84. package/dist/tools/pr-analysis.js +169 -0
  85. package/dist/tools/pr-analysis.js.map +1 -0
  86. package/dist/tools/propagation.d.ts +74 -0
  87. package/dist/tools/propagation.d.ts.map +1 -0
  88. package/dist/tools/propagation.js +194 -0
  89. package/dist/tools/propagation.js.map +1 -0
  90. package/dist/tools/questions.d.ts +80 -0
  91. package/dist/tools/questions.d.ts.map +1 -0
  92. package/dist/tools/questions.js +213 -0
  93. package/dist/tools/questions.js.map +1 -0
  94. package/dist/tools/refusal-fetch.d.ts +71 -0
  95. package/dist/tools/refusal-fetch.d.ts.map +1 -0
  96. package/dist/tools/refusal-fetch.js +137 -0
  97. package/dist/tools/refusal-fetch.js.map +1 -0
  98. package/dist/tools/scope.d.ts +60 -0
  99. package/dist/tools/scope.d.ts.map +1 -0
  100. package/dist/tools/scope.js +160 -0
  101. package/dist/tools/scope.js.map +1 -0
  102. package/dist/tools/similar-incidents.d.ts +105 -0
  103. package/dist/tools/similar-incidents.d.ts.map +1 -0
  104. package/dist/tools/similar-incidents.js +228 -0
  105. package/dist/tools/similar-incidents.js.map +1 -0
  106. package/dist/tools/validate.d.ts +103 -0
  107. package/dist/tools/validate.d.ts.map +1 -0
  108. package/dist/tools/validate.js +176 -0
  109. package/dist/tools/validate.js.map +1 -0
  110. package/dist/tools/verification-status.d.ts +68 -0
  111. package/dist/tools/verification-status.d.ts.map +1 -0
  112. package/dist/tools/verification-status.js +179 -0
  113. package/dist/tools/verification-status.js.map +1 -0
  114. package/dist/transport.d.ts +58 -0
  115. package/dist/transport.d.ts.map +1 -0
  116. package/dist/transport.js +115 -0
  117. package/dist/transport.js.map +1 -0
  118. package/package.json +33 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `refusal_fetch` — the fourteenth endpoint (`ai-layer-plan.md` §3), and the
3
+ * new work that endpoint's own re-derivation pointed at. §4 technique 4:
4
+ * *"Refusals summarised, never enumerated. A count, one verbatim exemplar, a
5
+ * handle to fetch the rest."*
6
+ *
7
+ * The logic underneath — `summariseRefusals`/`fetchRefusalPage`
8
+ * (`@descryy/core`'s `query/refusal-fetch.ts`) — is already built and
9
+ * unit-tested; this is the tool boundary its own header names as still open:
10
+ * *"An MCP surface that wants a prettier, shorter token... is free to wrap
11
+ * this — that is a presentation choice for P27."* No wrapping was needed —
12
+ * the handle is already opaque and self-describing — so this tool is thin:
13
+ * read the population, hand it to whichever of the two functions the call
14
+ * shape asks for.
15
+ *
16
+ * ## Two calls, one tool — same pattern `questions.ts` already uses
17
+ *
18
+ * No `handle` argument → a fresh summary (`summariseRefusals`): cheap, one
19
+ * count, one exemplar, a handle. A `handle` argument → redeem it for a real
20
+ * page (`fetchRefusalPage`). This mirrors `questions.ts`'s own "no `answer`
21
+ * argument is a pure read" split rather than inventing a second shape.
22
+ *
23
+ * ## The call-site trap this tool must not reproduce
24
+ *
25
+ * `refusal-fetch.ts`'s own header names it explicitly: the store reader
26
+ * defaults to `classified: true`, which silently drops every unclassified
27
+ * row (DEC-243's grouper only wants the classifiable subset). This tool
28
+ * always asks for `{ classified: false }` — the complete ledger, classified
29
+ * and unclassified alike — because a `refusal_fetch` that only showed the
30
+ * askable subset would be the empty-denominator defect wearing this
31
+ * technique's clothes, exactly what that module's header warns against.
32
+ *
33
+ * ## No cap on the population read
34
+ *
35
+ * `provider.unresolvedRefs({ classified: false })` is called with no
36
+ * `limit` — the handle's population fingerprint has to be stable between the
37
+ * summary call and every page redemption, and a capped read here would make
38
+ * "the ledger changed" indistinguishable from "the cap sampled differently
39
+ * this time." Correctness over a cheap shortcut, same choice `refusal-fetch.ts`
40
+ * itself already made.
41
+ */
42
+ import { type ToolDefinition } from "./kit.ts";
43
+ export interface RefusalFetchSummaryItem {
44
+ readonly fromNodeId: string;
45
+ readonly edgeType: string;
46
+ readonly rawTarget: string;
47
+ readonly file: string | null;
48
+ readonly line: number | null;
49
+ readonly producedBy: string;
50
+ readonly reason: string;
51
+ }
52
+ export interface RefusalFetchSummaryData {
53
+ readonly kind: "summary";
54
+ /** The honest denominator — every unresolved row in the graph, classified and unclassified alike. */
55
+ readonly count: number;
56
+ /** One row's `reason`, verbatim — never re-worded (§4 technique 4). `null` only when `count === 0`. */
57
+ readonly exemplar: string | null;
58
+ /** Redeem with a follow-up call passing this as `handle`. `null` only when `count === 0`. */
59
+ readonly handle: string | null;
60
+ }
61
+ export interface RefusalFetchPageData {
62
+ readonly kind: "page";
63
+ readonly items: readonly RefusalFetchSummaryItem[];
64
+ /** Restated on every page — the truncation rule pointed the other way (§5.5). */
65
+ readonly total: number;
66
+ /** Redeem for the next page. `null` on the last page. */
67
+ readonly nextHandle: string | null;
68
+ }
69
+ export type RefusalFetchData = RefusalFetchSummaryData | RefusalFetchPageData;
70
+ export declare const refusalFetchTool: ToolDefinition;
71
+ //# sourceMappingURL=refusal-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal-fetch.d.ts","sourceRoot":"","sources":["../../src/tools/refusal-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAqBlB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uGAAuG;IACvG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACnD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,MAAM,gBAAgB,GAAG,uBAAuB,GAAG,oBAAoB,CAAC;AAiF9E,eAAO,MAAM,gBAAgB,EAAE,cAe9B,CAAC"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * `refusal_fetch` — the fourteenth endpoint (`ai-layer-plan.md` §3), and the
3
+ * new work that endpoint's own re-derivation pointed at. §4 technique 4:
4
+ * *"Refusals summarised, never enumerated. A count, one verbatim exemplar, a
5
+ * handle to fetch the rest."*
6
+ *
7
+ * The logic underneath — `summariseRefusals`/`fetchRefusalPage`
8
+ * (`@descryy/core`'s `query/refusal-fetch.ts`) — is already built and
9
+ * unit-tested; this is the tool boundary its own header names as still open:
10
+ * *"An MCP surface that wants a prettier, shorter token... is free to wrap
11
+ * this — that is a presentation choice for P27."* No wrapping was needed —
12
+ * the handle is already opaque and self-describing — so this tool is thin:
13
+ * read the population, hand it to whichever of the two functions the call
14
+ * shape asks for.
15
+ *
16
+ * ## Two calls, one tool — same pattern `questions.ts` already uses
17
+ *
18
+ * No `handle` argument → a fresh summary (`summariseRefusals`): cheap, one
19
+ * count, one exemplar, a handle. A `handle` argument → redeem it for a real
20
+ * page (`fetchRefusalPage`). This mirrors `questions.ts`'s own "no `answer`
21
+ * argument is a pure read" split rather than inventing a second shape.
22
+ *
23
+ * ## The call-site trap this tool must not reproduce
24
+ *
25
+ * `refusal-fetch.ts`'s own header names it explicitly: the store reader
26
+ * defaults to `classified: true`, which silently drops every unclassified
27
+ * row (DEC-243's grouper only wants the classifiable subset). This tool
28
+ * always asks for `{ classified: false }` — the complete ledger, classified
29
+ * and unclassified alike — because a `refusal_fetch` that only showed the
30
+ * askable subset would be the empty-denominator defect wearing this
31
+ * technique's clothes, exactly what that module's header warns against.
32
+ *
33
+ * ## No cap on the population read
34
+ *
35
+ * `provider.unresolvedRefs({ classified: false })` is called with no
36
+ * `limit` — the handle's population fingerprint has to be stable between the
37
+ * summary call and every page redemption, and a capped read here would make
38
+ * "the ledger changed" indistinguishable from "the cap sampled differently
39
+ * this time." Correctness over a cheap shortcut, same choice `refusal-fetch.ts`
40
+ * itself already made.
41
+ */
42
+ import { fetchRefusalPage, summariseRefusals } from "@descryy/core";
43
+ import { answer, optionalInteger, optionalString, } from "./kit.js";
44
+ const SCHEMA = {
45
+ type: "object",
46
+ properties: {
47
+ handle: {
48
+ type: "string",
49
+ description: "Redeem a handle from a prior refusal_fetch call to fetch the next page of rows. Omit for " +
50
+ "a fresh summary — count, one exemplar reason, and a handle to start paging.",
51
+ },
52
+ pageSize: {
53
+ type: "integer",
54
+ minimum: 1,
55
+ description: "Rows per page when minting a fresh handle (a summary call). Default 50. Ignored on a paged call — the page size travels inside the handle itself.",
56
+ },
57
+ },
58
+ additionalProperties: false,
59
+ };
60
+ function brief(ref) {
61
+ return {
62
+ fromNodeId: ref.fromNodeId,
63
+ edgeType: ref.edgeType,
64
+ rawTarget: ref.rawTarget,
65
+ file: ref.file,
66
+ line: ref.line,
67
+ producedBy: ref.producedBy,
68
+ reason: ref.reason,
69
+ };
70
+ }
71
+ async function run(args, ctx) {
72
+ const handle = optionalString(args, "handle");
73
+ const pageSize = optionalInteger(args, "pageSize", 1);
74
+ const base = ctx.session.provider().baseStamp();
75
+ ctx.progress("Reading the unresolved-reference ledger");
76
+ const { refs } = ctx.session.provider().unresolvedRefs({ classified: false });
77
+ const envelopeBase = {
78
+ resolutionFloor: 0, // a ledger read, not a graph traversal claim — same convention as similar_incidents' R0 floor
79
+ commitSha: base.commitSha,
80
+ graphBuiltAt: base.graphBuiltAt,
81
+ irSchemaVersion: base.irSchemaVersion,
82
+ nameLevel: true,
83
+ };
84
+ if (handle === undefined) {
85
+ ctx.progress(`Summarising ${refs.length} unresolved reference(s)`);
86
+ const summary = summariseRefusals(refs, pageSize);
87
+ return answer({
88
+ ...envelopeBase,
89
+ state: summary.count === 0 ? "empty" : "ok",
90
+ headline: summary.count === 0
91
+ ? "No unresolved references are recorded — nothing for refusal_fetch to page through."
92
+ : `${summary.count} unresolved reference(s) recorded. Exemplar: "${summary.exemplar}". Call again with { "handle": "${summary.handle}" } to page through the rest.`,
93
+ notes: summary.count === 0
94
+ ? ["The graph has no unresolved-reference rows at all — every edge either resolved or was never attempted."]
95
+ : [],
96
+ data: { kind: "summary", count: summary.count, exemplar: summary.exemplar, handle: summary.handle },
97
+ });
98
+ }
99
+ ctx.progress("Redeeming the refusal_fetch handle");
100
+ const page = fetchRefusalPage(refs, handle);
101
+ if (page.state === "refused") {
102
+ return answer({
103
+ ...envelopeBase,
104
+ state: "refused",
105
+ headline: "This refusal_fetch handle could not be redeemed.",
106
+ notes: [page.reason],
107
+ data: { kind: "page", items: [], total: 0, nextHandle: null },
108
+ });
109
+ }
110
+ return answer({
111
+ ...envelopeBase,
112
+ headline: `Page of ${page.items.length} unresolved reference(s), of ${page.total} total.`,
113
+ notes: [],
114
+ data: {
115
+ kind: "page",
116
+ items: page.items.map(brief),
117
+ total: page.total,
118
+ nextHandle: page.nextHandle,
119
+ },
120
+ });
121
+ }
122
+ export const refusalFetchTool = {
123
+ name: "refusal_fetch",
124
+ class: "read",
125
+ tier: "evidence",
126
+ version: "1.0.0",
127
+ title: "Page through the graph's recorded refusals",
128
+ description: "The graph's full ledger of unresolved references — edges an adapter could not resolve, with " +
129
+ "the reason recorded rather than guessed. Call with no arguments for a one-line summary (count, " +
130
+ "one exemplar reason, a handle); call again with that handle to page through every row. Never " +
131
+ "enumerates in full on the first call — §4 technique 4: summarise, don't enumerate. A handle " +
132
+ "issued against a population that has since changed (a re-analyse ran) refuses rather than " +
133
+ "silently returning a wrong page.",
134
+ inputSchema: SCHEMA,
135
+ run,
136
+ };
137
+ //# sourceMappingURL=refusal-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal-fetch.js","sourceRoot":"","sources":["../../src/tools/refusal-fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `scope` — "I am about to change this. What else does it touch?"
3
+ *
4
+ * §12.5's diff scoping, exposed. Expansion stops on **score**, not hop count:
5
+ * a delete reaching a payment gateway four hops out matters and a logging import
6
+ * one hop out does not, and hop-count scoping cannot tell those apart.
7
+ *
8
+ * ## Files in, node ids out — and the misses are part of the answer
9
+ *
10
+ * A developer names files; scoping seeds on nodes. A path in the request that
11
+ * the graph has never heard of is the interesting case, and it has two very
12
+ * different causes that look identical from here: the file is not analysable by
13
+ * any configured adapter, or the graph is stale and predates the file. Both mean
14
+ * the blast radius omits it, so both are disclosed. Reporting "0 affected" for a
15
+ * file the graph does not contain would be the exact confirmed-clean-versus-not-
16
+ * analysable collapse §20.2 exists to prevent.
17
+ */
18
+ import { type ScopeTier } from "@descryy/core";
19
+ import type { EdgeType } from "@descryy/ir";
20
+ import { type BriefNode, type ToolDefinition } from "./kit.ts";
21
+ /** A node in the scope, with why it is in it. */
22
+ export interface ScopedNodeReport extends BriefNode {
23
+ readonly score: number;
24
+ readonly tier: ScopeTier;
25
+ readonly depth: number;
26
+ /** The edge type this node was reached through. Absent for a seed. */
27
+ readonly via?: EdgeType;
28
+ }
29
+ /** Tier 1 is the changed set itself; 2-4 are the scoring bands beneath it. */
30
+ export interface ScopeTierCount {
31
+ readonly tier: number;
32
+ readonly count: number;
33
+ }
34
+ export interface ScopeReport {
35
+ readonly changed: readonly ScopedNodeReport[];
36
+ /** Capped by `limit`; `affectedTotal` is the uncapped count. */
37
+ readonly affected: readonly ScopedNodeReport[];
38
+ readonly affectedTotal: number;
39
+ readonly droppedForBudget: number;
40
+ readonly budget: number;
41
+ readonly budgetUsed: number;
42
+ readonly byTier: readonly ScopeTierCount[];
43
+ }
44
+ /**
45
+ * Nothing the caller named is in the graph, so the traversal never ran.
46
+ *
47
+ * A genuinely different shape rather than a `ScopeReport` full of zeroes, and
48
+ * the absent keys are the tell: there is no `budget` because nothing was
49
+ * spent, and no `affectedTotal` because nothing was counted. `changed` and
50
+ * `affected` are always empty here and `seeds` is always 0 — narrow on
51
+ * `"seeds" in data`.
52
+ */
53
+ export interface ScopeNotSeeded {
54
+ readonly changed: readonly ScopedNodeReport[];
55
+ readonly affected: readonly ScopedNodeReport[];
56
+ readonly seeds: number;
57
+ }
58
+ export type ScopeData = ScopeReport | ScopeNotSeeded;
59
+ export declare const scopeTool: ToolDefinition;
60
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/tools/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAA8C,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAC3F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAOL,KAAK,SAAS,EAEd,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAGlB,iDAAiD;AACjD,MAAM,WAAW,gBAAiB,SAAQ,SAAS;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,cAAc,CAAC;AA0JrD,eAAO,MAAM,SAAS,EAAE,cAavB,CAAC"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `scope` — "I am about to change this. What else does it touch?"
3
+ *
4
+ * §12.5's diff scoping, exposed. Expansion stops on **score**, not hop count:
5
+ * a delete reaching a payment gateway four hops out matters and a logging import
6
+ * one hop out does not, and hop-count scoping cannot tell those apart.
7
+ *
8
+ * ## Files in, node ids out — and the misses are part of the answer
9
+ *
10
+ * A developer names files; scoping seeds on nodes. A path in the request that
11
+ * the graph has never heard of is the interesting case, and it has two very
12
+ * different causes that look identical from here: the file is not analysable by
13
+ * any configured adapter, or the graph is stale and predates the file. Both mean
14
+ * the blast radius omits it, so both are disclosed. Reporting "0 affected" for a
15
+ * file the graph does not contain would be the exact confirmed-clean-versus-not-
16
+ * analysable collapse §20.2 exists to prevent.
17
+ */
18
+ import { nodesInFiles, scopeChange } from "@descryy/core";
19
+ import { answer, briefNode, optionalEnum, optionalInteger, optionalStringArray, ToolInputError, } from "./kit.js";
20
+ const TRIGGERS = ["pre-push", "pr", "nightly"];
21
+ const SCHEMA = {
22
+ type: "object",
23
+ properties: {
24
+ files: {
25
+ type: "array",
26
+ items: { type: "string" },
27
+ description: "Repository-relative paths that are changing. Every symbol declared in them seeds the " +
28
+ "blast radius.",
29
+ },
30
+ nodeIds: {
31
+ type: "array",
32
+ items: { type: "string" },
33
+ description: "Graph node ids to seed with directly, instead of or as well as files.",
34
+ },
35
+ trigger: {
36
+ type: "string",
37
+ enum: [...TRIGGERS],
38
+ description: "Which node budget to use: pre-push 200, pr 1000, nightly 5000. Default pr.",
39
+ },
40
+ budget: {
41
+ type: "integer",
42
+ minimum: 1,
43
+ description: "Override the trigger's node budget.",
44
+ },
45
+ limit: {
46
+ type: "integer",
47
+ minimum: 1,
48
+ description: "Cap how many affected nodes come back in the reply. Default 25 (MK-9 — the same walk-cost " +
49
+ "reduction impact.ts applies, for the same reason). A capped reply always discloses " +
50
+ "shown/total; raise this explicitly when the full set is needed.",
51
+ },
52
+ },
53
+ additionalProperties: false,
54
+ };
55
+ function shape(entry) {
56
+ return {
57
+ ...briefNode(entry.node),
58
+ score: Number(entry.score.toFixed(4)),
59
+ tier: entry.tier,
60
+ depth: entry.depth,
61
+ ...(entry.via === null ? {} : { via: entry.via.type }),
62
+ };
63
+ }
64
+ async function run(args, ctx) {
65
+ const files = optionalStringArray(args, "files") ?? [];
66
+ const seedIds = [...(optionalStringArray(args, "nodeIds") ?? [])];
67
+ const trigger = optionalEnum(args, "trigger", TRIGGERS) ?? "pr";
68
+ const budget = optionalInteger(args, "budget", 1);
69
+ const limit = optionalInteger(args, "limit", 1) ?? 25;
70
+ if (files.length === 0 && seedIds.length === 0) {
71
+ throw new ToolInputError('give at least one of "files" or "nodeIds"');
72
+ }
73
+ const notes = [];
74
+ const session = ctx.session;
75
+ if (files.length > 0) {
76
+ ctx.progress(`Resolving ${files.length} path(s) to graph nodes`);
77
+ const absolute = files.filter((f) => f.startsWith("/"));
78
+ if (absolute.length > 0) {
79
+ notes.push(`${absolute.length} path(s) were absolute. The graph stores repository-relative paths, so ` +
80
+ "these matched nothing. Pass paths relative to the repository root.");
81
+ }
82
+ const { nodes, missing } = nodesInFiles(session.store().driver, files);
83
+ for (const node of nodes)
84
+ seedIds.push(node.id);
85
+ if (missing.length > 0) {
86
+ notes.push(`${missing.length} of ${files.length} path(s) have no node in the graph ` +
87
+ `(${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", …" : ""}). Either no ` +
88
+ "configured adapter reads them or the graph predates them — run analyze. Their impact " +
89
+ "is not in this answer and they are not reported as having none.");
90
+ }
91
+ }
92
+ if (seedIds.length === 0) {
93
+ const base = session.provider().baseStamp();
94
+ return answer({
95
+ headline: "Nothing in the graph matches what you named, so no blast radius could be computed.",
96
+ state: "refused",
97
+ nameLevel: true,
98
+ resolutionFloor: 0,
99
+ commitSha: base.commitSha,
100
+ graphBuiltAt: base.graphBuiltAt,
101
+ irSchemaVersion: base.irSchemaVersion,
102
+ notes: [
103
+ ...notes,
104
+ "This is not a result of zero impact. No seed node was found, so the traversal never ran.",
105
+ ],
106
+ data: { changed: [], affected: [], seeds: 0 },
107
+ });
108
+ }
109
+ ctx.progress(`Expanding from ${seedIds.length} changed symbol(s)`);
110
+ const scoped = scopeChange(session.provider(), [...new Set(seedIds)], {
111
+ trigger,
112
+ ...(budget === undefined ? {} : { budget }),
113
+ });
114
+ const affected = scoped.data.affected;
115
+ const shown = affected.slice(0, limit);
116
+ if (shown.length < affected.length) {
117
+ notes.push(`${affected.length - shown.length} lower-scoring affected node(s) are in the scope but not ` +
118
+ `listed here — the reply is capped at ${limit}. Raise "limit" to see them; they were ` +
119
+ "analysed either way.");
120
+ }
121
+ const byTier = [1, 2, 3, 4].map((tier) => ({
122
+ tier,
123
+ count: tier === 1
124
+ ? scoped.data.changed.length
125
+ : affected.filter((node) => node.tier === tier).length,
126
+ }));
127
+ return {
128
+ result: {
129
+ ...scoped,
130
+ data: {
131
+ changed: scoped.data.changed.map(shape),
132
+ affected: shown.map(shape),
133
+ affectedTotal: affected.length,
134
+ droppedForBudget: scoped.data.dropped.length,
135
+ budget: scoped.data.budget,
136
+ budgetUsed: scoped.data.budgetUsed,
137
+ byTier,
138
+ },
139
+ notes: [...scoped.notes, ...notes],
140
+ },
141
+ ...(affected.length === 0 ? { state: "empty" } : {}),
142
+ nameLevel: true,
143
+ headline: `${scoped.data.changed.length} changed symbol(s) reach ${affected.length} other node(s) ` +
144
+ `above the impact threshold, under the ${trigger} budget of ${scoped.data.budget}.`,
145
+ };
146
+ }
147
+ export const scopeTool = {
148
+ name: "scope",
149
+ class: "read",
150
+ tier: "evidence",
151
+ version: "1.0.0",
152
+ title: "Blast radius of a change",
153
+ description: "Given the files or symbols you are changing, return what else is affected, ranked by impact " +
154
+ "score and grouped into tiers 1-4. Expansion stops on score rather than hop count, so a " +
155
+ "distant high-impact dependency is kept and a nearby trivial one is dropped. Names the paths " +
156
+ "it could not find in the graph rather than reporting them as unaffected.",
157
+ inputSchema: SCHEMA,
158
+ run,
159
+ };
160
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/tools/scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `similar_incidents` — "have we seen this failure before?", asked about a
3
+ * failure that is **not yet in the graph**.
4
+ *
5
+ * ## Why this is a separate tool from `impact`'s `related` field
6
+ *
7
+ * `rootCauseSearch` — the `related` list every `impact` reply carries — walks
8
+ * `INCIDENT_CORRELATED`/`FIXED_BY` edges out of a node that already exists.
9
+ * It cannot help with a failure observed a moment ago: a stack trace and a set
10
+ * of touched files have no node, so there is nothing to walk from. This tool
11
+ * is the other direction. It takes the failure as a description and ranks the
12
+ * `INCIDENT` nodes the graph already holds against it, using the two signals
13
+ * `similarIncidents` (`@descryy/core`, DEC-274 step 2) computes from data the
14
+ * graph already has: file-set Jaccard over `INCIDENT_CORRELATED`, and
15
+ * token-set Jaccard over the incident's own text. Deterministic, no vector
16
+ * store, no model call.
17
+ *
18
+ * ## The disclosure this tool exists to carry, and it is not optional
19
+ *
20
+ * DEC-279 makes one sentence mandatory here, and DEC-274 is why. That
21
+ * measurement ran the correlation machinery over `descry-core`,
22
+ * `descry-runtime` and `descry-adapters` — 734, 459 and 203 commits — and
23
+ * found **zero `INCIDENT` nodes in all three**. Not few: zero. Only an
24
+ * explicit `git revert` mints an `INCIDENT` today; a fix-forward commit, which
25
+ * is how almost every real bug is actually fixed, mints nothing at all.
26
+ *
27
+ * So this matcher's **recall is not computable — 0/0, not 0%**. It has never
28
+ * been handed a real incident to find, and no number describes how many it
29
+ * would miss. An empty result therefore means *"no linked incident in this
30
+ * graph, and this graph has few or no incidents"* and must never be read as
31
+ * *"no similar incident exists."* That is the same *absence is not evidence*
32
+ * obligation `impact` already carries on every reply, and `similar-incidents.test.ts`
33
+ * fails if it goes missing from either the empty or the non-empty branch.
34
+ *
35
+ * ## Two different absences, and they are not the same result
36
+ *
37
+ * `ai-tool-contract.md` §1.3a splits them, and the split is the whole reason
38
+ * this endpoint is interesting:
39
+ *
40
+ * - **Incidents exist and none matched.** The denominator is real, the tool
41
+ * looked completely, and the absence is a fact about the code. `state:
42
+ * "empty"`, per §1.1's split.
43
+ * - **The graph holds zero `INCIDENT` nodes.** There is no denominator.
44
+ * Nothing was looked at, and the reason is Descry's producer coverage
45
+ * rather than anything about this repository: only `git revert` mints an
46
+ * `INCIDENT` and fix-forward mints nothing. Answering "empty" there would
47
+ * say *"this code has no similar incidents"* when the true statement is
48
+ * *"Descry cannot see most incidents in any codebase"* — the
49
+ * empty-denominator defect inside the endpoint written to demonstrate the
50
+ * rule against it. So it is a refusal, carried as `state: "refused"` (§1.1:
51
+ * `refused` is a result state, and `not analysable` is a report category —
52
+ * different axes, never this).
53
+ *
54
+ * §1.3a generalises it past this tool: **when a query's population is itself
55
+ * zero, the answer is a refusal, never an empty result.** That puts this in
56
+ * line with its siblings rather than apart from them — `contracts` refuses on
57
+ * a graph with no endpoint, `propagation` when no walked edge type touches
58
+ * the start node.
59
+ *
60
+ * The denominator travels in the payload either way. `incidentsInGraph` is
61
+ * the population and `analysed` is false only when it is zero, so a caller
62
+ * can tell the two absences apart **from the payload alone, with no string
63
+ * matching on prose** — §1's distinguishability requirement, satisfied
64
+ * without waiting for the wider envelope.
65
+ */
66
+ import { type SimilarIncidentSignalKind } from "@descryy/core";
67
+ import { type BriefNode, type ToolDefinition } from "./kit.ts";
68
+ /** One signal that fired for a candidate — the "why", never a bare score. */
69
+ export interface SimilarIncidentSignalReport {
70
+ readonly kind: SimilarIncidentSignalKind;
71
+ /** Jaccard similarity for this signal alone, 0..1. */
72
+ readonly score: number;
73
+ }
74
+ export interface SimilarIncidentReport {
75
+ readonly incident: BriefNode;
76
+ /** The `FIX_PATTERN` this incident resolved to, when a `FIXED_BY` edge exists. */
77
+ readonly fix: BriefNode | null;
78
+ readonly signals: readonly SimilarIncidentSignalReport[];
79
+ /** Weighted combination of every signal the query attempted, 0..1. */
80
+ readonly score: number;
81
+ }
82
+ /**
83
+ * One shape, both branches.
84
+ *
85
+ * `analysed: false` with `incidentsInGraph: 0` is the zero-denominator case
86
+ * and arrives with `state: "refused"`; every other reply is `ok`.
87
+ * The two counts are in the payload rather than only in the prose because a
88
+ * caller deciding what to say about an empty list needs the denominator, and
89
+ * a sentence it has to parse is not a denominator.
90
+ */
91
+ export interface SimilarIncidentsData {
92
+ readonly query: {
93
+ readonly touchedFiles: readonly string[];
94
+ readonly errorMessage: string | null;
95
+ };
96
+ readonly matches: readonly SimilarIncidentReport[];
97
+ /** Count before `limit` — honest about what was capped. */
98
+ readonly matchesTotal: number;
99
+ /** How many `INCIDENT` nodes were available to compare against. Zero is the headline fact. */
100
+ readonly incidentsInGraph: number;
101
+ /** False when the graph holds no `INCIDENT` node at all: nothing was compared. */
102
+ readonly analysed: boolean;
103
+ }
104
+ export declare const similarIncidentsTool: ToolDefinition;
105
+ //# sourceMappingURL=similar-incidents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similar-incidents.d.ts","sourceRoot":"","sources":["../../src/tools/similar-incidents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,EAAoB,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EAOL,KAAK,SAAS,EAEd,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAiClB,6EAA6E;AAC7E,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;QACzC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAiJD,eAAO,MAAM,oBAAoB,EAAE,cAkBlC,CAAC"}