specguard-mcp 0.1.4 → 0.1.5

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 (29) hide show
  1. package/README.md +150 -1
  2. package/dist/src/support/specguard-api.d.ts +19 -0
  3. package/dist/src/support/specguard-api.js +26 -0
  4. package/dist/src/support/specguard-api.js.map +1 -1
  5. package/dist/src/tools/add-repository-member.d.ts +31 -0
  6. package/dist/src/tools/add-repository-member.js +105 -0
  7. package/dist/src/tools/add-repository-member.js.map +1 -0
  8. package/dist/src/tools/args.d.ts +26 -0
  9. package/dist/src/tools/args.js +52 -0
  10. package/dist/src/tools/args.js.map +1 -1
  11. package/dist/src/tools/index.d.ts +53 -9
  12. package/dist/src/tools/index.js +65 -9
  13. package/dist/src/tools/index.js.map +1 -1
  14. package/dist/src/tools/list-repository-members.d.ts +32 -0
  15. package/dist/src/tools/list-repository-members.js +76 -0
  16. package/dist/src/tools/list-repository-members.js.map +1 -0
  17. package/dist/src/tools/near-duplicate-clusters.d.ts +58 -0
  18. package/dist/src/tools/near-duplicate-clusters.js +116 -0
  19. package/dist/src/tools/near-duplicate-clusters.js.map +1 -0
  20. package/dist/src/tools/remove-repository-member.d.ts +30 -0
  21. package/dist/src/tools/remove-repository-member.js +93 -0
  22. package/dist/src/tools/remove-repository-member.js.map +1 -0
  23. package/dist/src/tools/rename-repository.d.ts +44 -0
  24. package/dist/src/tools/rename-repository.js +99 -0
  25. package/dist/src/tools/rename-repository.js.map +1 -0
  26. package/dist/src/tools/update-repository-member-permissions.d.ts +30 -0
  27. package/dist/src/tools/update-repository-member-permissions.js +97 -0
  28. package/dist/src/tools/update-repository-member-permissions.js.map +1 -0
  29. package/package.json +2 -1
@@ -1,11 +1,17 @@
1
1
  import addRepository from "./add-repository.js";
2
+ import addRepositoryMember from "./add-repository-member.js";
2
3
  import createRepositoryApiKey from "./create-repository-api-key.js";
3
4
  import lintIntentAnnotations from "./lint-intent-annotations.js";
4
5
  import listRepositories from "./list-repositories.js";
6
+ import listRepositoryMembers from "./list-repository-members.js";
7
+ import nearDuplicateClusters from "./near-duplicate-clusters.js";
5
8
  import getRepositoryOverview from "./repository-overview.js";
6
9
  import registrableRepositories from "./registrable-repositories.js";
7
10
  import removeRepository from "./remove-repository.js";
11
+ import removeRepositoryMember from "./remove-repository-member.js";
12
+ import renameRepository from "./rename-repository.js";
8
13
  import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
14
+ import updateRepositoryMemberPermissions from "./update-repository-member-permissions.js";
9
15
  /**
10
16
  * THE REGISTRY — the one file that changes when the toolset grows.
11
17
  *
@@ -41,12 +47,21 @@ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
41
47
  *
42
48
  * == What is deliberately absent
43
49
  *
44
- * `/check-intent` and duplicate clustering are NOT here and must not be added
45
- * until their backing engine and data exist (SPGD-114 / SPGD-115). A tool
46
- * advertised in `tools/list` is a promise an agent will act on: wrapping an
47
- * endpoint that does not exist would produce a server that discovers cleanly
48
- * and fails on use, which is worse than not offering the tool, because the
49
- * agent has already committed to a plan by the time it finds out.
50
+ * `/check-intent` is NOT here and must not be added until its backing endpoint
51
+ * exists — `specguard/config/routes.rb:113` carries it as a comment only, which
52
+ * is the evidence this forbid rests on. A tool advertised in `tools/list` is a
53
+ * promise an agent will act on: wrapping an endpoint that does not exist would
54
+ * produce a server that discovers cleanly and fails on use, which is worse than
55
+ * not offering the tool, because the agent has already committed to a plan by
56
+ * the time it finds out.
57
+ *
58
+ * Duplicate clustering was once under this same forbid, on the same standing
59
+ * rule — no tool may wrap what has not shipped. That half retired when the
60
+ * platform moved: SPGD-703 (`specguard` `c43dc19`, 2026-08-28) shipped
61
+ * `GET /api/v1/repository?near_duplicates=`, serving
62
+ * `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask, and
63
+ * `near_duplicate_clusters` below wraps it. What moved was the platform, not
64
+ * the bar.
50
65
  *
51
66
  * == The fourth: the first tool that WRITES
52
67
  *
@@ -106,9 +121,44 @@ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
106
121
  * `requestJson`'s JSON parse.
107
122
  *
108
123
  * The standing rule itself is unchanged and still binding — which still keeps
109
- * out `/check-intent`, duplicate clustering, member management and rename: none
110
- * of their backing endpoints has shipped, and a tool advertised in `tools/list`
111
- * remains a promise an agent will act on.
124
+ * out `/check-intent`: it has no backing endpoint (`routes.rb:113` mounts it
125
+ * as a comment only). A tool advertised in `tools/list` remains a promise an
126
+ * agent will act on.
127
+ *
128
+ * == The ninth through twelfth: member management
129
+ *
130
+ * - `list_repository_members`, `add_repository_member`,
131
+ * `update_repository_member_permissions` and `remove_repository_member`
132
+ * wrap the four `user_repository_members` endpoints (all shipped: SPGD-875,
133
+ * specguard@origin/main).
134
+ *
135
+ * They also forced the transport's fourth verb: `update` is a PATCH, which
136
+ * arrived with `patchJson` in this same slice — 200 with a JSON body, so it
137
+ * routes through `requestJson` like `postJson` rather than `deleteJson`'s
138
+ * raw-body handling.
139
+ *
140
+ * A known limitation rides with them and is stated in the edit/revoke tool
141
+ * descriptions rather than papered over: PATCH and DELETE name a membership by
142
+ * id, but no endpoint serves that id (the platform's `#serialize` deliberately
143
+ * omits it), so the id comes from the platform's web members page today. That
144
+ * is a platform-side follow-up, not a client-side workaround — there is no
145
+ * name-based lookup.
146
+ *
147
+ * == The thirteenth: rename
148
+ *
149
+ * - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped:
150
+ * SPGD-878, `specguard@origin/main` e026793, PR #266).
151
+ *
152
+ * This is the entry the forbid above once named as "the natural next slice" —
153
+ * the platform moved (SPGD-878 shipped the endpoint), so the forbid retired
154
+ * exactly as the duplicate-clustering one did. It reuses `patchJson` landed
155
+ * with the member-management slice rather than minting a transport of its own.
156
+ * The verb's one asymmetry is carried in the tool description rather than
157
+ * papered over: rename authorizes OWNER-ONLY, while removal authorizes the
158
+ * BROADER `repo.delete` capability — a member who may destroy the repository
159
+ * may not rename it. Before this tool the bridge's only rename path was
160
+ * remove-and-re-register, which destroys every key, run and intent; this one
161
+ * keeps them.
112
162
  */
113
163
  export const TOOLS = [
114
164
  lintIntentAnnotations,
@@ -119,5 +169,11 @@ export const TOOLS = [
119
169
  removeRepository,
120
170
  createRepositoryApiKey,
121
171
  revokeRepositoryApiKey,
172
+ nearDuplicateClusters,
173
+ listRepositoryMembers,
174
+ addRepositoryMember,
175
+ updateRepositoryMemberPermissions,
176
+ removeRepositoryMember,
177
+ renameRepository,
122
178
  ];
123
179
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;CACvB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,mBAAmB,MAAM,4BAA4B,CAAC;AAC7D,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,+BAA+B,CAAC;AACnE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,iCAAiC,MAAM,2CAA2C,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmJG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iCAAiC;IACjC,sBAAsB;IACtB,gBAAgB;CACjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
4
+ * platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
5
+ * SPGD-875).
6
+ *
7
+ * == Memberships only, never `keys_minted`
8
+ *
9
+ * The endpoint answers the same rows the web members page renders — `handle`,
10
+ * `permissions`, `granted_by`, `created_at`, ordered by handle — and
11
+ * deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
12
+ * page gates separately; this read answers memberships to a `members.manage`
13
+ * holder and no more. A tool that promised counts here would be promising
14
+ * something the endpoint refuses to say.
15
+ *
16
+ * == No membership id, by design
17
+ *
18
+ * The body carries no membership id: ids are not portable between
19
+ * repositories, and serving one would invite treating it as portable. PATCH
20
+ * and DELETE still name rows by that id — see
21
+ * `update_repository_member_permissions` and `remove_repository_member` for
22
+ * where the id comes from today.
23
+ *
24
+ * == No client-side capability probing
25
+ *
26
+ * The server gates at `current_repository(:members_manage)`: a non-member gets
27
+ * 404 (the repository's existence stays hidden), a member without the
28
+ * capability gets 403 with SpecGuard's own sentence. This tool predicts
29
+ * neither; it reports what came back.
30
+ */
31
+ declare const listRepositoryMembers: ToolDefinition;
32
+ export default listRepositoryMembers;
@@ -0,0 +1,76 @@
1
+ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { requireString } from "./args.js";
3
+ /**
4
+ * `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
5
+ * platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
6
+ * SPGD-875).
7
+ *
8
+ * == Memberships only, never `keys_minted`
9
+ *
10
+ * The endpoint answers the same rows the web members page renders — `handle`,
11
+ * `permissions`, `granted_by`, `created_at`, ordered by handle — and
12
+ * deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
13
+ * page gates separately; this read answers memberships to a `members.manage`
14
+ * holder and no more. A tool that promised counts here would be promising
15
+ * something the endpoint refuses to say.
16
+ *
17
+ * == No membership id, by design
18
+ *
19
+ * The body carries no membership id: ids are not portable between
20
+ * repositories, and serving one would invite treating it as portable. PATCH
21
+ * and DELETE still name rows by that id — see
22
+ * `update_repository_member_permissions` and `remove_repository_member` for
23
+ * where the id comes from today.
24
+ *
25
+ * == No client-side capability probing
26
+ *
27
+ * The server gates at `current_repository(:members_manage)`: a non-member gets
28
+ * 404 (the repository's existence stays hidden), a member without the
29
+ * capability gets 403 with SpecGuard's own sentence. This tool predicts
30
+ * neither; it reports what came back.
31
+ */
32
+ const listRepositoryMembers = {
33
+ name: "list_repository_members",
34
+ title: "List repository members",
35
+ description: "Lists who has access to a SpecGuard repository: one row per member with their `handle`, " +
36
+ "`permissions`, `granted_by` (who last set them) and `created_at`, ordered by handle. " +
37
+ "The list answers MEMBERSHIPS only and never reports how many CI keys a member has minted " +
38
+ "(`keys_minted`) — that is a separate `keys.manage` disclosure this endpoint deliberately " +
39
+ "withholds, and the API-keys tools are the surface for it. " +
40
+ "Authorization is the `members.manage` capability: a caller who is not a member of the " +
41
+ "repository is refused 404 (the repository's existence stays hidden), and a member without " +
42
+ "`members.manage` is refused 403 in SpecGuard's own words. " +
43
+ "Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
44
+ "handle. " +
45
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `list_repositories` " +
46
+ "reads and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ repository_id: {
51
+ type: "string",
52
+ description: "The repository whose members to list — its numeric id, as `add_repository` " +
53
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
54
+ },
55
+ },
56
+ required: ["repository_id"],
57
+ // Closed for the reason every tool here states: `server.ts` forwards
58
+ // `arguments` unvalidated, so an open schema would let a misspelled argument
59
+ // be dropped silently.
60
+ additionalProperties: false,
61
+ },
62
+ async run(args, context) {
63
+ const repositoryId = requireString(args["repository_id"], "repository_id");
64
+ const api = requireUserApiConfig(context.config);
65
+ const members = await getJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members`, {}, context.fetch);
66
+ // Passed through unreshaped, the standing rule (`types.ts`: "A thin client
67
+ // that reshapes its upstream is not thin") — the endpoint serves the same
68
+ // four fields the web members page renders, under the same names.
69
+ return {
70
+ text: JSON.stringify(members, null, 2),
71
+ structured: members,
72
+ };
73
+ },
74
+ };
75
+ export default listRepositoryMembers;
76
+ //# sourceMappingURL=list-repository-members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-repository-members.js","sourceRoot":"","sources":["../../../src/tools/list-repository-members.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,0FAA0F;QAC1F,uFAAuF;QACvF,2FAA2F;QAC3F,2FAA2F;QAC3F,4DAA4D;QAC5D,wFAAwF;QACxF,4FAA4F;QAC5F,4DAA4D;QAC5D,yFAAyF;QACzF,UAAU;QACV,uFAAuF;QACvF,yFAAyF;IAC3F,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,6EAA6E;oBAC7E,qEAAqE;aACxE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,6EAA6E;QAC7E,uBAAuB;QACvB,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAE3E,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,UAAU,EAClE,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,0EAA0E;QAC1E,kEAAkE;QAClE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
4
+ * platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
5
+ * `near_duplicates` block to `RepositoryOverview` behind an opt-in ask.
6
+ *
7
+ * == What the block is, and why it is behind an ask at all
8
+ *
9
+ * It is the suite-wide near-duplicate census: which tests READ alike — same
10
+ * body text, not same file — clustered by the engine SPGD-369 shipped
11
+ * (`specguard` `f7d5352`). It is the first block on that endpoint whose grain
12
+ * is the REPOSITORY rather than a run or a window of runs, and the first one
13
+ * gated on COST rather than rows: `NearDuplicateClusters` is linear but
14
+ * measured in seconds (seven queries at every size; seconds at a few thousand
15
+ * identities, extrapolating to tens of seconds at the 20,000-identity design
16
+ * point). `?near_duplicates=` confines that cost to the client that named it —
17
+ * no ask, key present and `null`, not one query — which is why this block is a
18
+ * SEPARATE tool rather than a forwarded parameter on
19
+ * `get_repository_overview`: an agent calling the overview should never pay
20
+ * the census by accident, and an agent calling this tool has asked for nothing
21
+ * else. Splitting the tools splits the cost along exactly the line the server
22
+ * drew.
23
+ *
24
+ * == The ask is always sent, and always spelled `"true"`
25
+ *
26
+ * The server reads only whether the parameter is PRESENT
27
+ * (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
28
+ * exactly as `=true` does, and a non-String shape is read as no ask at all.
29
+ * There is no "off" value for a client to send, so this tool has no arguments
30
+ * — nothing about the census is choosable from here, which is also why the
31
+ * schema is CLOSED rather than merely empty: `server.ts` forwards `arguments`
32
+ * unvalidated, and an open schema would let an invented argument ride through
33
+ * and be silently dropped (see `registrable-repositories.ts` for the same
34
+ * call). `near_duplicates: "true"` is built rather than stringified for the
35
+ * same reason `repository-overview.ts` builds its `unannotated_examples` key:
36
+ * `getJson` omits only `undefined`, so a conditional send is the only honest
37
+ * way to spell "always" here.
38
+ *
39
+ * == The response is passed through, not re-modelled
40
+ *
41
+ * Same rule as `repository-overview.ts`: every figure in the block is
42
+ * annotated in `RepositoryOverview#serialized_near_duplicates` with the reason
43
+ * for its shape, and several of those reasons are about honesty rather than
44
+ * convenience — `similarity_floor`/`similarity_basis` served FIRST because a
45
+ * count without the statement of what the similarity means is a vacuous
46
+ * figure; `member_count` (texts in the REPOSITORY, across every run) and
47
+ * `example_count` (examples in the ONE run `weighed_run_id` names) served side
48
+ * by side because a three-example table-driven loop is one member and three
49
+ * examples, and flattening them would erase the figure the ranking is built
50
+ * on; `unobserved_members` disclosing that the member list holds an identity
51
+ * the weighed run did not observe; `similarity_range` as the `[strongest,
52
+ * weakest]` pair because membership is transitive while similarity is not.
53
+ * Reshaping here would discard distinctions the serializer spent that care
54
+ * preserving, so the body goes back as it arrived — the whole body, which
55
+ * carries the repository/run context the clusters sit inside.
56
+ */
57
+ declare const nearDuplicateClusters: ToolDefinition;
58
+ export default nearDuplicateClusters;
@@ -0,0 +1,116 @@
1
+ import { requireApiConfig } from "../config.js";
2
+ import { getJsonObject } from "../support/specguard-api.js";
3
+ /**
4
+ * `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
5
+ * platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
6
+ * `near_duplicates` block to `RepositoryOverview` behind an opt-in ask.
7
+ *
8
+ * == What the block is, and why it is behind an ask at all
9
+ *
10
+ * It is the suite-wide near-duplicate census: which tests READ alike — same
11
+ * body text, not same file — clustered by the engine SPGD-369 shipped
12
+ * (`specguard` `f7d5352`). It is the first block on that endpoint whose grain
13
+ * is the REPOSITORY rather than a run or a window of runs, and the first one
14
+ * gated on COST rather than rows: `NearDuplicateClusters` is linear but
15
+ * measured in seconds (seven queries at every size; seconds at a few thousand
16
+ * identities, extrapolating to tens of seconds at the 20,000-identity design
17
+ * point). `?near_duplicates=` confines that cost to the client that named it —
18
+ * no ask, key present and `null`, not one query — which is why this block is a
19
+ * SEPARATE tool rather than a forwarded parameter on
20
+ * `get_repository_overview`: an agent calling the overview should never pay
21
+ * the census by accident, and an agent calling this tool has asked for nothing
22
+ * else. Splitting the tools splits the cost along exactly the line the server
23
+ * drew.
24
+ *
25
+ * == The ask is always sent, and always spelled `"true"`
26
+ *
27
+ * The server reads only whether the parameter is PRESENT
28
+ * (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
29
+ * exactly as `=true` does, and a non-String shape is read as no ask at all.
30
+ * There is no "off" value for a client to send, so this tool has no arguments
31
+ * — nothing about the census is choosable from here, which is also why the
32
+ * schema is CLOSED rather than merely empty: `server.ts` forwards `arguments`
33
+ * unvalidated, and an open schema would let an invented argument ride through
34
+ * and be silently dropped (see `registrable-repositories.ts` for the same
35
+ * call). `near_duplicates: "true"` is built rather than stringified for the
36
+ * same reason `repository-overview.ts` builds its `unannotated_examples` key:
37
+ * `getJson` omits only `undefined`, so a conditional send is the only honest
38
+ * way to spell "always" here.
39
+ *
40
+ * == The response is passed through, not re-modelled
41
+ *
42
+ * Same rule as `repository-overview.ts`: every figure in the block is
43
+ * annotated in `RepositoryOverview#serialized_near_duplicates` with the reason
44
+ * for its shape, and several of those reasons are about honesty rather than
45
+ * convenience — `similarity_floor`/`similarity_basis` served FIRST because a
46
+ * count without the statement of what the similarity means is a vacuous
47
+ * figure; `member_count` (texts in the REPOSITORY, across every run) and
48
+ * `example_count` (examples in the ONE run `weighed_run_id` names) served side
49
+ * by side because a three-example table-driven loop is one member and three
50
+ * examples, and flattening them would erase the figure the ranking is built
51
+ * on; `unobserved_members` disclosing that the member list holds an identity
52
+ * the weighed run did not observe; `similarity_range` as the `[strongest,
53
+ * weakest]` pair because membership is transitive while similarity is not.
54
+ * Reshaping here would discard distinctions the serializer spent that care
55
+ * preserving, so the body goes back as it arrived — the whole body, which
56
+ * carries the repository/run context the clusters sit inside.
57
+ */
58
+ const nearDuplicateClusters = {
59
+ name: "near_duplicate_clusters",
60
+ title: "Near-duplicate clusters",
61
+ description: "Run SpecGuard's near-duplicate census over a repository's tests — which tests READ alike " +
62
+ "(same body text, whatever file they sit in), clustered by similarity. Answers the refactoring " +
63
+ "question the overview's per-run rankings cannot: where is the same test written twice, before " +
64
+ "you delete or merge anything. " +
65
+ "THIS IS THE EXPENSIVE READ ON THIS BRIDGE: the census is linear but measured in seconds — seven " +
66
+ "queries at every size, tens of seconds extrapolated at the 20,000-test design point — which is " +
67
+ "exactly why the server serves it only to a client that asks (`?near_duplicates=`) and answers " +
68
+ "`near_duplicates: null` on the plain overview. Calling this tool IS the ask; it takes no " +
69
+ "arguments because nothing about the census is choosable — the clusters are the repository's, " +
70
+ "computed over every run, and one call returns them all. " +
71
+ "READ THE DISCLOSURE KEYS BEFORE THE COUNT: `similarity_floor` and `similarity_basis` sit FIRST " +
72
+ "in the block and qualify every cluster below them — a cluster count without what 'similar' " +
73
+ "meant is a figure you cannot act on. `truncated: true` means the cluster list was cut at the " +
74
+ "cap while the counts above it (`cluster_count`, `identity_count`, `clustered_*`) describe the " +
75
+ "WHOLE census, so never fold `clusters` length as the total. " +
76
+ "READ EVERY CLUSTER'S FIGURES AT THEIR OWN GRAIN: `member_count` counts texts in the REPOSITORY " +
77
+ "across every run, `example_count` counts examples in the ONE run `weighed_run_id` names — a " +
78
+ "three-example table-driven loop is ONE member and THREE examples, and those two numbers beside " +
79
+ "each other are the whole point. `unobserved_members: true` on a cluster says a member identity " +
80
+ "the weighed run did not observe (deleted, renamed, deselected) is still listed — do not reconcile " +
81
+ "the member list against a run's examples and expect it to balance. `similarity_range` is " +
82
+ "[strongest, weakest]: membership is transitive, similarity is not, and the gap between the edges " +
83
+ "is the merge risk. `total_seconds` is raw and `null` where nothing was timed — never a zero " +
84
+ "that would read as free. " +
85
+ "A quiet answer is a FINDING, not a gap: `clusters: []` with a real `identity_count` is the " +
86
+ "success state (nothing reads alike), and the three silences — nothing ingested " +
87
+ "(`recorded_count: 0`), nothing embedded (`identity_count: 0`), nothing alike — are kept " +
88
+ "distinguishable by those counts rather than collapsed into one empty list. " +
89
+ "Same credential and endpoint as `get_repository_overview` (an `sgk_` repository key on " +
90
+ "`GET /api/v1/repository`); the response is that endpoint's full body with the `near_duplicates` " +
91
+ "block OPENED, passed through unmodified.",
92
+ inputSchema: {
93
+ type: "object",
94
+ // No properties, deliberately — see this file's header. Still CLOSED rather
95
+ // than merely empty, for the reason `registrable-repositories.ts` gives
96
+ // inline: `server.ts` forwards `arguments` unvalidated and `run` ignores
97
+ // them, so an open schema would have an invented argument silently dropped
98
+ // and the call answered as if it had been honoured.
99
+ additionalProperties: false,
100
+ },
101
+ async run(_args, context) {
102
+ const api = requireApiConfig(context.config);
103
+ const overview = await getJsonObject(api, "/api/v1/repository", {
104
+ // Always sent, always `"true"` — the server reads only that the key is
105
+ // present (`?near_duplicates=false` opens the block too), and this
106
+ // tool exists to open it. See this file's header.
107
+ near_duplicates: "true",
108
+ }, context.fetch);
109
+ return {
110
+ text: JSON.stringify(overview, null, 2),
111
+ structured: overview,
112
+ };
113
+ },
114
+ };
115
+ export default nearDuplicateClusters;
116
+ //# sourceMappingURL=near-duplicate-clusters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"near-duplicate-clusters.js","sourceRoot":"","sources":["../../../src/tools/near-duplicate-clusters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAE/B,KAAK,EAAE,yBAAyB;IAEhC,WAAW,EACT,2FAA2F;QAC3F,gGAAgG;QAChG,gGAAgG;QAChG,gCAAgC;QAChC,kGAAkG;QAClG,iGAAiG;QACjG,gGAAgG;QAChG,2FAA2F;QAC3F,+FAA+F;QAC/F,0DAA0D;QAC1D,iGAAiG;QACjG,6FAA6F;QAC7F,+FAA+F;QAC/F,gGAAgG;QAChG,8DAA8D;QAC9D,iGAAiG;QACjG,8FAA8F;QAC9F,iGAAiG;QACjG,iGAAiG;QACjG,oGAAoG;QACpG,2FAA2F;QAC3F,mGAAmG;QACnG,8FAA8F;QAC9F,2BAA2B;QAC3B,6FAA6F;QAC7F,iFAAiF;QACjF,0FAA0F;QAC1F,6EAA6E;QAC7E,yFAAyF;QACzF,kGAAkG;QAClG,0CAA0C;IAE5C,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,oDAAoD;QACpD,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;QACtB,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE7C,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,GAAG,EACH,oBAAoB,EACpB;YACE,uEAAuE;YACvE,mEAAmE;YACnE,kDAAkD;YAClD,eAAe,EAAE,MAAM;SACxB,EACD,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,UAAU,EAAE,QAAQ;SACrB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `DELETE /api/v1/repositories/:repository_id/members/:id` as a tool — shipped
4
+ * in the platform (`specguard/config/routes.rb`,
5
+ * `Api::V1::UserRepositoryMembersController#destroy`, SPGD-875).
6
+ *
7
+ * == The two documented asymmetries, stated BEFORE the call
8
+ *
9
+ * Revoking a membership deliberately LEAVES that member's minted CI keys
10
+ * authenticating (`User has_many :created_api_keys, dependent: :nullify`) —
11
+ * the lever for those is the API-keys surface, not this one. And
12
+ * self-revocation is permitted: it is the caller's own access to give up, and
13
+ * after it the next request to these routes answers 404, not 403 (a former
14
+ * member is a non-member). Both belong in the description because they are
15
+ * the facts an agent must know before it commits to the call.
16
+ *
17
+ * == The owner row cannot arrive
18
+ *
19
+ * An owner membership is structurally impossible
20
+ * (`RepositoryMembership#user_is_not_the_owner`), so "cannot remove the owner"
21
+ * is a model invariant, not a guard this bridge re-checks.
22
+ *
23
+ * == 204 with no body
24
+ *
25
+ * The same empty-body success `remove_repository` and
26
+ * `revoke_repository_api_key` serve; the same reason `deleteJson` exists
27
+ * rather than routing a 204 through `requestJson`'s JSON parse.
28
+ */
29
+ declare const removeRepositoryMember: ToolDefinition;
30
+ export default removeRepositoryMember;
@@ -0,0 +1,93 @@
1
+ import { deleteJson, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { requireString } from "./args.js";
3
+ /**
4
+ * `DELETE /api/v1/repositories/:repository_id/members/:id` as a tool — shipped
5
+ * in the platform (`specguard/config/routes.rb`,
6
+ * `Api::V1::UserRepositoryMembersController#destroy`, SPGD-875).
7
+ *
8
+ * == The two documented asymmetries, stated BEFORE the call
9
+ *
10
+ * Revoking a membership deliberately LEAVES that member's minted CI keys
11
+ * authenticating (`User has_many :created_api_keys, dependent: :nullify`) —
12
+ * the lever for those is the API-keys surface, not this one. And
13
+ * self-revocation is permitted: it is the caller's own access to give up, and
14
+ * after it the next request to these routes answers 404, not 403 (a former
15
+ * member is a non-member). Both belong in the description because they are
16
+ * the facts an agent must know before it commits to the call.
17
+ *
18
+ * == The owner row cannot arrive
19
+ *
20
+ * An owner membership is structurally impossible
21
+ * (`RepositoryMembership#user_is_not_the_owner`), so "cannot remove the owner"
22
+ * is a model invariant, not a guard this bridge re-checks.
23
+ *
24
+ * == 204 with no body
25
+ *
26
+ * The same empty-body success `remove_repository` and
27
+ * `revoke_repository_api_key` serve; the same reason `deleteJson` exists
28
+ * rather than routing a 204 through `requestJson`'s JSON parse.
29
+ */
30
+ const removeRepositoryMember = {
31
+ name: "remove_repository_member",
32
+ title: "Remove repository member",
33
+ description: "Revokes one person's access to a SpecGuard repository by removing their membership. " +
34
+ "The member is named by `member_id` — a MEMBERSHIP id, not a user id — scoped to " +
35
+ "`repository_id`: a membership id belonging to a different repository is refused 404, " +
36
+ "never a cross-repository revoke. " +
37
+ "⚠️ Revoking does NOT revoke that member's minted CI keys — any sgk_… keys they created " +
38
+ "on the repository keep authenticating by design; the lever for those is the API-keys " +
39
+ "surface (`revoke_repository_api_key`), not this one. " +
40
+ "Self-revocation is permitted: a caller may remove their own membership, and their next " +
41
+ "request to the member routes answers 404 — a former member is a non-member, so this tool " +
42
+ "cannot read the repository's members afterwards. The repository owner's membership cannot " +
43
+ "be removed at all (an owner holds everything by construction). " +
44
+ "KNOWN LIMITATION: no API endpoint serves the membership id — the member list and the " +
45
+ "add-member response both omit it by design — so the id must be obtained from the " +
46
+ "platform (today via the repository's web members page); there is no name-based lookup. " +
47
+ "Authorization is the `members.manage` capability — a member without it is refused 403 in " +
48
+ "SpecGuard's own words. A 204 means the membership is revoked. " +
49
+ "Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
50
+ "handle. " +
51
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
52
+ "writes with and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ repository_id: {
57
+ type: "string",
58
+ description: "The repository the membership belongs to — its numeric id, as `add_repository` " +
59
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
60
+ },
61
+ member_id: {
62
+ type: "string",
63
+ description: "The id of the MEMBERSHIP row to revoke — not a user id, and not the handle. " +
64
+ "Scoped to `repository_id`: a foreign membership id is refused 404. No API endpoint " +
65
+ "serves this id today; obtain it from the platform's web members page.",
66
+ },
67
+ },
68
+ required: ["repository_id", "member_id"],
69
+ // Closed for the reason every tool here states — and on a DESTRUCTIVE path,
70
+ // a silently dropped misspelled argument must not be able to leave the
71
+ // agent believing it revoked a membership the call never named.
72
+ additionalProperties: false,
73
+ },
74
+ async run(args, context) {
75
+ const repositoryId = requireString(args["repository_id"], "repository_id");
76
+ const memberId = requireString(args["member_id"], "member_id");
77
+ const api = requireUserApiConfig(context.config);
78
+ const body = await deleteJson(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members/${encodeURIComponent(memberId)}`, context.fetch);
79
+ // 204 with no body — the tool's own words, because the deployment's whole
80
+ // answer was "no content". The keys asymmetry is restated here because the
81
+ // moment of success is the last moment the fact still matters.
82
+ return {
83
+ text: body === ""
84
+ ? `Membership ${memberId} revoked (204). That person's minted CI keys on this ` +
85
+ "repository keep authenticating — revoke those separately with " +
86
+ "revoke_repository_api_key if that is the intent."
87
+ : body,
88
+ structured: { repository_id: repositoryId, member_id: memberId, revoked: true },
89
+ };
90
+ },
91
+ };
92
+ export default removeRepositoryMember;
93
+ //# sourceMappingURL=remove-repository-member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove-repository-member.js","sourceRoot":"","sources":["../../../src/tools/remove-repository-member.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,sFAAsF;QACtF,kFAAkF;QAClF,uFAAuF;QACvF,mCAAmC;QACnC,yFAAyF;QACzF,uFAAuF;QACvF,uDAAuD;QACvD,yFAAyF;QACzF,2FAA2F;QAC3F,4FAA4F;QAC5F,iEAAiE;QACjE,uFAAuF;QACvF,mFAAmF;QACnF,yFAAyF;QACzF,2FAA2F;QAC3F,gEAAgE;QAChE,yFAAyF;QACzF,UAAU;QACV,oFAAoF;QACpF,+FAA+F;IACjG,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,iFAAiF;oBACjF,qEAAqE;aACxE;YACD,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,8EAA8E;oBAC9E,qFAAqF;oBACrF,uEAAuE;aAC1E;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,EAAE,WAAW,CAAC;QACxC,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC;QAE/D,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAG,MAAM,UAAU,CAC3B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,YAAY,kBAAkB,CAAC,QAAQ,CAAC,EAAE,EAClG,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,0EAA0E;QAC1E,2EAA2E;QAC3E,+DAA+D;QAC/D,OAAO;YACL,IAAI,EACF,IAAI,KAAK,EAAE;gBACT,CAAC,CAAC,cAAc,QAAQ,uDAAuD;oBAC7E,gEAAgE;oBAChE,kDAAkD;gBACpD,CAAC,CAAC,IAAI;YACV,UAAU,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE;SAChF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `PATCH /api/v1/repositories/:id` as a tool — shipped in the platform
4
+ * (`specguard/config/routes.rb:186`, `Api::V1::UserRepositoriesController#update`,
5
+ * SPGD-878, PR #266).
6
+ *
7
+ * == Why this tool exists at all
8
+ *
9
+ * Before it, the only way to change a repository's `org/repo` name through the
10
+ * bridge was `remove_repository` plus `add_repository` — which destroys every
11
+ * API key, every run and every intent on the repository, because that is what
12
+ * remove does. The platform's rename endpoint keeps all of it; this tool is
13
+ * the bridge's one-line path to that.
14
+ *
15
+ * == Authorization is OWNER-ONLY — deliberately narrower than delete
16
+ *
17
+ * The controller gates through `current_repository(:owner)`, NOT the
18
+ * `repo.delete` capability fork `remove_repository` authorizes through — so a
19
+ * member granted `repo.delete` may DESTROY the repository but may NOT rename
20
+ * it. The asymmetry is the platform's, and the description carries it rather
21
+ * than papering over it.
22
+ *
23
+ * == The grant precondition
24
+ *
25
+ * The owner check redeems a browser-issued grant that is valid for seven days;
26
+ * a nil or stale one is a 403 whose own sentence names the fix (re-grant via
27
+ * the browser). The bridge surfaces that sentence verbatim and adds no
28
+ * client-side grant handling — the server is the gate.
29
+ *
30
+ * == The body is top-level `{github_full_name}` — NOT nested
31
+ *
32
+ * `update_params` permits `github_full_name` at the TOP level, not under a
33
+ * `repository` key; a nested body is silently empty to the server. And a taken
34
+ * name is a 400 (`render_bad_request`, the `taken` branch), not a 409 — this
35
+ * description says so because it is the one answer an agent is most likely to
36
+ * mispredict.
37
+ *
38
+ * == The 200 body is the list_repositories shape
39
+ *
40
+ * `{repository: serialize(...)}` — the same serializer `list_repositories`
41
+ * serves, so the response needs no reshaping and gets none.
42
+ */
43
+ declare const renameRepository: ToolDefinition;
44
+ export default renameRepository;