specguard-mcp 0.1.2 → 0.1.3
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/README.md +81 -15
- package/dist/bin/specguard-mcp.js +5 -0
- package/dist/bin/specguard-mcp.js.map +1 -1
- package/dist/src/support/run-command.d.ts +42 -0
- package/dist/src/support/run-command.js +124 -8
- package/dist/src/support/run-command.js.map +1 -1
- package/dist/src/support/specguard-api.d.ts +33 -0
- package/dist/src/support/specguard-api.js +119 -8
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/support/teardown.d.ts +33 -0
- package/dist/src/support/teardown.js +56 -0
- package/dist/src/support/teardown.js.map +1 -0
- package/dist/src/tools/add-repository.d.ts +55 -0
- package/dist/src/tools/add-repository.js +114 -0
- package/dist/src/tools/add-repository.js.map +1 -0
- package/dist/src/tools/args.d.ts +20 -0
- package/dist/src/tools/args.js +30 -0
- package/dist/src/tools/args.js.map +1 -1
- package/dist/src/tools/index.d.ts +20 -7
- package/dist/src/tools/index.js +22 -7
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +14 -7
- package/dist/src/tools/list-repositories.js +14 -7
- package/dist/src/tools/list-repositories.js.map +1 -1
- package/dist/src/tools/repository-overview.js +33 -8
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAkB,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kill the runs we started before going away.
|
|
3
|
+
*
|
|
4
|
+
* `run-command.ts` spawns every run `detached`, which is what lets a timeout
|
|
5
|
+
* signal the whole process tree rather than only the process we forked. The
|
|
6
|
+
* cost is that a detached child is in its own session, outside this server's
|
|
7
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
8
|
+
* a supervisor's `kill -- -PGID` — does not reach a lint run in flight. Without
|
|
9
|
+
* the handlers below such a run is not merely orphaned but UNBOUNDED: the
|
|
10
|
+
* `DEFAULT_COMMAND_TIMEOUT_MS` ceiling is a parent-side timer, so killing the
|
|
11
|
+
* parent destroys the only thing that was ever going to stop it, and a lint of a
|
|
12
|
+
* 20k-example suite goes on burning CPU with nobody waiting for its answer.
|
|
13
|
+
*
|
|
14
|
+
* == Why this is not written inline in `bin/specguard-mcp.ts`
|
|
15
|
+
*
|
|
16
|
+
* It lives here so a test can install the REAL handler. `bin/` runs `main()` on
|
|
17
|
+
* import, so a test that imported it would connect a transport rather than
|
|
18
|
+
* exercise a teardown, and the alternative — retyping the handler inside a test
|
|
19
|
+
* fixture — would assert that a COPY of the logic works while the shipped one
|
|
20
|
+
* went unread. `bin/` is left as the one place the policy is applied, which is
|
|
21
|
+
* the same split it already makes for the transport.
|
|
22
|
+
*
|
|
23
|
+
* DIAGNOSTICS GO TO STDERR, without exception. On stdio, stdout IS the JSON-RPC
|
|
24
|
+
* protocol channel: a line written there is framed as a message on the way out
|
|
25
|
+
* and corrupts the stream the client is still reading, surfacing as an
|
|
26
|
+
* unexplained disconnect rather than as the shutdown it actually was.
|
|
27
|
+
*
|
|
28
|
+
* NOTHING HERE WAITS. `killOutstandingRuns` sends SIGKILL and returns; SIGKILL
|
|
29
|
+
* cannot be refused, so there is no acknowledgement worth blocking a shutdown
|
|
30
|
+
* for. A teardown path that waits is a teardown path that can hang, which is the
|
|
31
|
+
* failure this handler exists to prevent rather than one to introduce.
|
|
32
|
+
*/
|
|
33
|
+
export declare function installTeardown(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { killOutstandingRuns } from "./run-command.js";
|
|
2
|
+
/**
|
|
3
|
+
* Kill the runs we started before going away.
|
|
4
|
+
*
|
|
5
|
+
* `run-command.ts` spawns every run `detached`, which is what lets a timeout
|
|
6
|
+
* signal the whole process tree rather than only the process we forked. The
|
|
7
|
+
* cost is that a detached child is in its own session, outside this server's
|
|
8
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
9
|
+
* a supervisor's `kill -- -PGID` — does not reach a lint run in flight. Without
|
|
10
|
+
* the handlers below such a run is not merely orphaned but UNBOUNDED: the
|
|
11
|
+
* `DEFAULT_COMMAND_TIMEOUT_MS` ceiling is a parent-side timer, so killing the
|
|
12
|
+
* parent destroys the only thing that was ever going to stop it, and a lint of a
|
|
13
|
+
* 20k-example suite goes on burning CPU with nobody waiting for its answer.
|
|
14
|
+
*
|
|
15
|
+
* == Why this is not written inline in `bin/specguard-mcp.ts`
|
|
16
|
+
*
|
|
17
|
+
* It lives here so a test can install the REAL handler. `bin/` runs `main()` on
|
|
18
|
+
* import, so a test that imported it would connect a transport rather than
|
|
19
|
+
* exercise a teardown, and the alternative — retyping the handler inside a test
|
|
20
|
+
* fixture — would assert that a COPY of the logic works while the shipped one
|
|
21
|
+
* went unread. `bin/` is left as the one place the policy is applied, which is
|
|
22
|
+
* the same split it already makes for the transport.
|
|
23
|
+
*
|
|
24
|
+
* DIAGNOSTICS GO TO STDERR, without exception. On stdio, stdout IS the JSON-RPC
|
|
25
|
+
* protocol channel: a line written there is framed as a message on the way out
|
|
26
|
+
* and corrupts the stream the client is still reading, surfacing as an
|
|
27
|
+
* unexplained disconnect rather than as the shutdown it actually was.
|
|
28
|
+
*
|
|
29
|
+
* NOTHING HERE WAITS. `killOutstandingRuns` sends SIGKILL and returns; SIGKILL
|
|
30
|
+
* cannot be refused, so there is no acknowledgement worth blocking a shutdown
|
|
31
|
+
* for. A teardown path that waits is a teardown path that can hang, which is the
|
|
32
|
+
* failure this handler exists to prevent rather than one to introduce.
|
|
33
|
+
*/
|
|
34
|
+
export function installTeardown() {
|
|
35
|
+
let tearingDown = false;
|
|
36
|
+
const teardown = (signal, status) => {
|
|
37
|
+
// A second Ctrl-C while the first is still unwinding must not re-enter the
|
|
38
|
+
// drain — the registry is already empty and the pids in it already spent.
|
|
39
|
+
if (tearingDown)
|
|
40
|
+
return;
|
|
41
|
+
tearingDown = true;
|
|
42
|
+
const killed = killOutstandingRuns();
|
|
43
|
+
if (killed > 0) {
|
|
44
|
+
process.stderr.write(`specguard-mcp: ${signal} received, killed ${killed} run${killed === 1 ? "" : "s"} still in flight\n`);
|
|
45
|
+
}
|
|
46
|
+
// The conventional 128 + signo, so a supervisor reads "died on SIGINT"
|
|
47
|
+
// rather than an ordinary failure. Exiting explicitly rather than restoring
|
|
48
|
+
// the default disposition and re-signalling ourselves: we hold no other
|
|
49
|
+
// teardown obligation, and an explicit status cannot be lost to a handler
|
|
50
|
+
// installed elsewhere.
|
|
51
|
+
process.exit(status);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", () => teardown("SIGINT", 130));
|
|
54
|
+
process.on("SIGTERM", () => teardown("SIGTERM", 143));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.js","sourceRoot":"","sources":["../../../src/support/teardown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,WAAW,GAAG,KAAK,CAAC;IAExB,MAAM,QAAQ,GAAG,CAAC,MAA4B,EAAE,MAAc,EAAE,EAAE;QAChE,2EAA2E;QAC3E,0EAA0E;QAC1E,IAAI,WAAW;YAAE,OAAO;QACxB,WAAW,GAAG,IAAI,CAAC;QAEnB,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAErC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;YACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kBAAkB,MAAM,qBAAqB,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,oBAAoB,CACtG,CAAC;QACJ,CAAC;QAED,uEAAuE;QACvE,4EAA4E;QAC5E,wEAAwE;QACxE,0EAA0E;QAC1E,uBAAuB;QACvB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `POST /api/v1/repositories` as a tool — shipped today in the platform
|
|
4
|
+
* (`specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
5
|
+
*
|
|
6
|
+
* == The first tool here that WRITES, and what that changed
|
|
7
|
+
*
|
|
8
|
+
* Everything before this read. The registry's standing rule is that a tool in
|
|
9
|
+
* `tools/list` is a promise an agent will act on, and this endpoint has existed
|
|
10
|
+
* on the platform for some time — what was missing was on THIS side: `getJson`
|
|
11
|
+
* hardcoded `method: "GET"` and took no body. `tools/index.ts` and
|
|
12
|
+
* `list-repositories.ts` both said so, and said the transport should land with
|
|
13
|
+
* the first write tool so it could be designed against a real request body and a
|
|
14
|
+
* real 4xx surface rather than invented for a caller that did not exist. This is
|
|
15
|
+
* that tool, and `postJson`/`postJsonObject` are that transport.
|
|
16
|
+
*
|
|
17
|
+
* == The request body is top-level, because the caller is an agent
|
|
18
|
+
*
|
|
19
|
+
* `{"github_full_name": "org/repo"}`, not `{"repository": {…}}`. The controller
|
|
20
|
+
* permits it that way and says why: this is a JSON API being driven by an agent,
|
|
21
|
+
* not a Rails form being submitted by a browser, and the top-level shape is what
|
|
22
|
+
* a caller writing curl by hand will send.
|
|
23
|
+
*
|
|
24
|
+
* == The format is NOT re-validated here, deliberately
|
|
25
|
+
*
|
|
26
|
+
* `Repository` validates `org/repo` itself, and a refusal now arrives through
|
|
27
|
+
* the 400 branch in `describeFailure` in SpecGuard's own words. A second format
|
|
28
|
+
* rule on this side is exactly what "a thin client that reshapes its upstream is
|
|
29
|
+
* not thin" forbids — it would be a rule with no owner, free to drift from the
|
|
30
|
+
* one that actually decides, and its divergence would surface as this bridge
|
|
31
|
+
* refusing a name the platform would have accepted. Checking that `full_name` is
|
|
32
|
+
* a present, non-blank string is the whole of the bridge's business: that is a
|
|
33
|
+
* shape check, which is why it is `requireString` from `args.ts` and not a
|
|
34
|
+
* hand-rolled one here.
|
|
35
|
+
*
|
|
36
|
+
* == The MODAL first answer is a 400, and it is the useful one
|
|
37
|
+
*
|
|
38
|
+
* `RepositoryRegistration::GrantVerifier` fails closed on a grant that is
|
|
39
|
+
* missing or stale, which is every person who has not opened SpecGuard in a
|
|
40
|
+
* browser since this shipped — the controller records that this is "an ordinary
|
|
41
|
+
* state and not an error". The sentence that comes back names the operator's
|
|
42
|
+
* exact next move (sign in, reconnect GitHub, retry), and reaching the agent
|
|
43
|
+
* intact is what the 400 branch in `specguard-api.ts` is for.
|
|
44
|
+
*
|
|
45
|
+
* == Why the description carries a hazard paragraph
|
|
46
|
+
*
|
|
47
|
+
* `types.ts` calls the description "prompt material, not documentation … the
|
|
48
|
+
* entire basis on which a model decides whether to call the tool". This tool is
|
|
49
|
+
* NOT idempotent and its 201 carries a reveal-once token, so an agent that
|
|
50
|
+
* learns those facts by hitting them has already lost the token. They are stated
|
|
51
|
+
* where they are read BEFORE the call is committed to, rather than left to be
|
|
52
|
+
* discovered from a failure.
|
|
53
|
+
*/
|
|
54
|
+
declare const addRepository: ToolDefinition;
|
|
55
|
+
export default addRepository;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
|
|
2
|
+
import { requireString } from "./args.js";
|
|
3
|
+
/**
|
|
4
|
+
* `POST /api/v1/repositories` as a tool — shipped today in the platform
|
|
5
|
+
* (`specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
6
|
+
*
|
|
7
|
+
* == The first tool here that WRITES, and what that changed
|
|
8
|
+
*
|
|
9
|
+
* Everything before this read. The registry's standing rule is that a tool in
|
|
10
|
+
* `tools/list` is a promise an agent will act on, and this endpoint has existed
|
|
11
|
+
* on the platform for some time — what was missing was on THIS side: `getJson`
|
|
12
|
+
* hardcoded `method: "GET"` and took no body. `tools/index.ts` and
|
|
13
|
+
* `list-repositories.ts` both said so, and said the transport should land with
|
|
14
|
+
* the first write tool so it could be designed against a real request body and a
|
|
15
|
+
* real 4xx surface rather than invented for a caller that did not exist. This is
|
|
16
|
+
* that tool, and `postJson`/`postJsonObject` are that transport.
|
|
17
|
+
*
|
|
18
|
+
* == The request body is top-level, because the caller is an agent
|
|
19
|
+
*
|
|
20
|
+
* `{"github_full_name": "org/repo"}`, not `{"repository": {…}}`. The controller
|
|
21
|
+
* permits it that way and says why: this is a JSON API being driven by an agent,
|
|
22
|
+
* not a Rails form being submitted by a browser, and the top-level shape is what
|
|
23
|
+
* a caller writing curl by hand will send.
|
|
24
|
+
*
|
|
25
|
+
* == The format is NOT re-validated here, deliberately
|
|
26
|
+
*
|
|
27
|
+
* `Repository` validates `org/repo` itself, and a refusal now arrives through
|
|
28
|
+
* the 400 branch in `describeFailure` in SpecGuard's own words. A second format
|
|
29
|
+
* rule on this side is exactly what "a thin client that reshapes its upstream is
|
|
30
|
+
* not thin" forbids — it would be a rule with no owner, free to drift from the
|
|
31
|
+
* one that actually decides, and its divergence would surface as this bridge
|
|
32
|
+
* refusing a name the platform would have accepted. Checking that `full_name` is
|
|
33
|
+
* a present, non-blank string is the whole of the bridge's business: that is a
|
|
34
|
+
* shape check, which is why it is `requireString` from `args.ts` and not a
|
|
35
|
+
* hand-rolled one here.
|
|
36
|
+
*
|
|
37
|
+
* == The MODAL first answer is a 400, and it is the useful one
|
|
38
|
+
*
|
|
39
|
+
* `RepositoryRegistration::GrantVerifier` fails closed on a grant that is
|
|
40
|
+
* missing or stale, which is every person who has not opened SpecGuard in a
|
|
41
|
+
* browser since this shipped — the controller records that this is "an ordinary
|
|
42
|
+
* state and not an error". The sentence that comes back names the operator's
|
|
43
|
+
* exact next move (sign in, reconnect GitHub, retry), and reaching the agent
|
|
44
|
+
* intact is what the 400 branch in `specguard-api.ts` is for.
|
|
45
|
+
*
|
|
46
|
+
* == Why the description carries a hazard paragraph
|
|
47
|
+
*
|
|
48
|
+
* `types.ts` calls the description "prompt material, not documentation … the
|
|
49
|
+
* entire basis on which a model decides whether to call the tool". This tool is
|
|
50
|
+
* NOT idempotent and its 201 carries a reveal-once token, so an agent that
|
|
51
|
+
* learns those facts by hitting them has already lost the token. They are stated
|
|
52
|
+
* where they are read BEFORE the call is committed to, rather than left to be
|
|
53
|
+
* discovered from a failure.
|
|
54
|
+
*/
|
|
55
|
+
const addRepository = {
|
|
56
|
+
name: "add_repository",
|
|
57
|
+
title: "Add repository",
|
|
58
|
+
description: "Registers a GitHub repository with SpecGuard for the person behind this server's user API " +
|
|
59
|
+
"key, and returns the repository along with its first CI API key. " +
|
|
60
|
+
"Takes `full_name` as `org/repo` — the same handle `list_repositories` reports and every other " +
|
|
61
|
+
"SpecGuard surface names a repository by. " +
|
|
62
|
+
"On success the response carries a `repository` block (`id`, `full_name`, `name`, " +
|
|
63
|
+
"`registered_at`) and an `api_key` block (`name`, `token`, `hint`, `created_at`). " +
|
|
64
|
+
"⚠️ `api_key.token` is shown THIS ONCE AND NEVER AGAIN — nothing stores it and no endpoint can " +
|
|
65
|
+
"re-serve it, so hand it to the user in your reply rather than assuming it can be fetched " +
|
|
66
|
+
"later. " +
|
|
67
|
+
"⚠️ This tool is NOT idempotent and it WRITES. If the call times out (SPECGUARD_TIMEOUT_MS) " +
|
|
68
|
+
"the registration may still have succeeded on the server, taking its one-time token with it; " +
|
|
69
|
+
"retrying then fails with `has already been taken`, which is the honest answer, and the " +
|
|
70
|
+
"recovery is SpecGuard's API-keys page in a browser. Do not retry a timeout blindly. " +
|
|
71
|
+
"Requires a CURRENT record of the caller's GitHub permissions, which only a browser session " +
|
|
72
|
+
"creates: a person who has not signed in to SpecGuard and connected GitHub recently is refused " +
|
|
73
|
+
"with a message saying exactly that, and the fix is theirs to perform in a browser — no " +
|
|
74
|
+
"argument to this tool can substitute for it. The repository must also be one the SpecGuard " +
|
|
75
|
+
"GitHub App is installed on and that this person administers. " +
|
|
76
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `list_repositories` reads " +
|
|
77
|
+
"and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
full_name: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "The repository to register, as `org/repo` (for example `acme/billing`) — the same " +
|
|
84
|
+
"handle `list_repositories` reports. Not a URL and not a bare repository name. " +
|
|
85
|
+
"SpecGuard validates the format and refuses an unusable one in its own words.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ["full_name"],
|
|
89
|
+
// Closed for the reason `list_repositories` states: `server.ts` forwards
|
|
90
|
+
// `arguments` unvalidated, so an open schema would let a misspelled argument
|
|
91
|
+
// be dropped silently and the call answered as though it had been honoured.
|
|
92
|
+
// On a WRITE that is worse than on a read — the call still registers
|
|
93
|
+
// something, just not what the agent believed it was asking for.
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
},
|
|
96
|
+
async run(args, context) {
|
|
97
|
+
// Argument shape FIRST, before the config is resolved and before anything is
|
|
98
|
+
// sent: a malformed call is the one failure the agent can fix unaided, and
|
|
99
|
+
// it must not cost a write attempt to discover.
|
|
100
|
+
const fullName = requireString(args["full_name"], "full_name");
|
|
101
|
+
const api = requireUserApiConfig(context.config);
|
|
102
|
+
const registration = await postJsonObject(api, "/api/v1/repositories", { github_full_name: fullName }, context.fetch);
|
|
103
|
+
// Passed through exactly as `list_repositories` passes its listing through.
|
|
104
|
+
// It matters more here: `api_key.token` exists nowhere else, so any reshaping
|
|
105
|
+
// on this hop is a value that cannot be recovered rather than a field that
|
|
106
|
+
// can be re-fetched.
|
|
107
|
+
return {
|
|
108
|
+
text: JSON.stringify(registration, null, 2),
|
|
109
|
+
structured: registration,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
export default addRepository;
|
|
114
|
+
//# sourceMappingURL=add-repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-repository.js","sourceRoot":"","sources":["../../../src/tools/add-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,aAAa,GAAmB;IACpC,IAAI,EAAE,gBAAgB;IACtB,KAAK,EAAE,gBAAgB;IACvB,WAAW,EACT,4FAA4F;QAC5F,mEAAmE;QACnE,gGAAgG;QAChG,2CAA2C;QAC3C,mFAAmF;QACnF,mFAAmF;QACnF,gGAAgG;QAChG,2FAA2F;QAC3F,SAAS;QACT,6FAA6F;QAC7F,8FAA8F;QAC9F,yFAAyF;QACzF,sFAAsF;QACtF,6FAA6F;QAC7F,gGAAgG;QAChG,yFAAyF;QACzF,6FAA6F;QAC7F,+DAA+D;QAC/D,6FAA6F;QAC7F,mFAAmF;IACrF,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,oFAAoF;oBACpF,gFAAgF;oBAChF,8EAA8E;aACjF;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,CAAC;QACvB,yEAAyE;QACzE,6EAA6E;QAC7E,4EAA4E;QAC5E,qEAAqE;QACrE,iEAAiE;QACjE,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,6EAA6E;QAC7E,2EAA2E;QAC3E,gDAAgD;QAChD,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,YAAY,GAAG,MAAM,cAAc,CACvC,GAAG,EACH,sBAAsB,EACtB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAC9B,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,4EAA4E;QAC5E,8EAA8E;QAC9E,2EAA2E;QAC3E,qBAAqB;QACrB,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3C,UAAU,EAAE,YAAY;SACzB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
package/dist/src/tools/args.d.ts
CHANGED
|
@@ -45,4 +45,24 @@
|
|
|
45
45
|
* A non-blank string, or nothing.
|
|
46
46
|
*/
|
|
47
47
|
export declare function optionalString(value: unknown, field: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* A non-blank string, and nothing else will do.
|
|
50
|
+
*
|
|
51
|
+
* The mandatory counterpart of `optionalString`, and it belongs here for the
|
|
52
|
+
* reason stated above rather than in the first tool that needs one: "is it
|
|
53
|
+
* present and a non-blank string" is a check about the SHAPE of a value, whose
|
|
54
|
+
* failure is always `ArgumentError`. Hand-rolling it inside a tool is the exact
|
|
55
|
+
* copy-paste this file was created to end — and the copy would have to re-pick
|
|
56
|
+
* the error class, which is the one thing the two `optionalString` copies got
|
|
57
|
+
* wrong.
|
|
58
|
+
*
|
|
59
|
+
* Trimmed like its optional sibling, and for the same reason: a value an agent
|
|
60
|
+
* produced by concatenating strings arrives with whitespace that is not part of
|
|
61
|
+
* what it meant to send.
|
|
62
|
+
*
|
|
63
|
+
* The two refusals are separate sentences on purpose. "You sent a number" and
|
|
64
|
+
* "you sent nothing" are different mistakes with different fixes, and a single
|
|
65
|
+
* message covering both would leave the agent to work out which it made.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requireString(value: unknown, field: string): string;
|
|
48
68
|
export declare function optionalBoolean(value: unknown, field: string): boolean | undefined;
|
package/dist/src/tools/args.js
CHANGED
|
@@ -56,6 +56,36 @@ export function optionalString(value, field) {
|
|
|
56
56
|
const trimmed = value.trim();
|
|
57
57
|
return trimmed === "" ? undefined : trimmed;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A non-blank string, and nothing else will do.
|
|
61
|
+
*
|
|
62
|
+
* The mandatory counterpart of `optionalString`, and it belongs here for the
|
|
63
|
+
* reason stated above rather than in the first tool that needs one: "is it
|
|
64
|
+
* present and a non-blank string" is a check about the SHAPE of a value, whose
|
|
65
|
+
* failure is always `ArgumentError`. Hand-rolling it inside a tool is the exact
|
|
66
|
+
* copy-paste this file was created to end — and the copy would have to re-pick
|
|
67
|
+
* the error class, which is the one thing the two `optionalString` copies got
|
|
68
|
+
* wrong.
|
|
69
|
+
*
|
|
70
|
+
* Trimmed like its optional sibling, and for the same reason: a value an agent
|
|
71
|
+
* produced by concatenating strings arrives with whitespace that is not part of
|
|
72
|
+
* what it meant to send.
|
|
73
|
+
*
|
|
74
|
+
* The two refusals are separate sentences on purpose. "You sent a number" and
|
|
75
|
+
* "you sent nothing" are different mistakes with different fixes, and a single
|
|
76
|
+
* message covering both would leave the agent to work out which it made.
|
|
77
|
+
*/
|
|
78
|
+
export function requireString(value, field) {
|
|
79
|
+
if (value === undefined || value === null) {
|
|
80
|
+
throw new ArgumentError(`\`${field}\` is required.`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "string")
|
|
83
|
+
throw new ArgumentError(`\`${field}\` must be a string.`);
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (trimmed === "")
|
|
86
|
+
throw new ArgumentError(`\`${field}\` must not be blank.`);
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
59
89
|
export function optionalBoolean(value, field) {
|
|
60
90
|
if (value === undefined || value === null)
|
|
61
91
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,KAAa;IACzD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IAEzF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAE/E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -41,13 +41,26 @@ import type { ToolDefinition } from "./types.js";
|
|
|
41
41
|
* and fails on use, which is worse than not offering the tool, because the
|
|
42
42
|
* agent has already committed to a plan by the time it finds out.
|
|
43
43
|
*
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
44
|
+
* == The fourth: the first tool that WRITES
|
|
45
|
+
*
|
|
46
|
+
* - `add_repository` wraps `POST /api/v1/repositories` (shipped:
|
|
47
|
+
* `specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
48
|
+
*
|
|
49
|
+
* The endpoint had shipped for a while; what this bridge lacked was a way to
|
|
50
|
+
* CALL it — `support/specguard-api.ts` offered only `getJson`, which hardcoded
|
|
51
|
+
* `method: "GET"` and took no body. The reservation recorded here was that the
|
|
52
|
+
* write transport should land WITH the first write tool, designed against a real
|
|
53
|
+
* request body and a real 4xx surface rather than invented for a caller that did
|
|
54
|
+
* not exist. That is what happened: `postJson`/`postJsonObject` arrived with
|
|
55
|
+
* this entry, sharing `fetchWithTimeout` with the read path rather than standing
|
|
56
|
+
* beside it, and `describeFailure` grew the `400` branch that surfaces
|
|
57
|
+
* SpecGuard's own refusal sentence — the modal answer this endpoint gives.
|
|
58
|
+
*
|
|
59
|
+
* The standing rule is unchanged and still binding, which is what keeps the rest
|
|
60
|
+
* of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
|
|
61
|
+
* the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
|
|
62
|
+
* wrapped here however useful a tool for them would be. What moved was the
|
|
63
|
+
* platform, not the bar.
|
|
51
64
|
*/
|
|
52
65
|
export declare const TOOLS: readonly ToolDefinition[];
|
|
53
66
|
export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
package/dist/src/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import addRepository from "./add-repository.js";
|
|
1
2
|
import lintIntentAnnotations from "./lint-intent-annotations.js";
|
|
2
3
|
import listRepositories from "./list-repositories.js";
|
|
3
4
|
import getRepositoryOverview from "./repository-overview.js";
|
|
@@ -43,17 +44,31 @@ import getRepositoryOverview from "./repository-overview.js";
|
|
|
43
44
|
* and fails on use, which is worse than not offering the tool, because the
|
|
44
45
|
* agent has already committed to a plan by the time it finds out.
|
|
45
46
|
*
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* == The fourth: the first tool that WRITES
|
|
48
|
+
*
|
|
49
|
+
* - `add_repository` wraps `POST /api/v1/repositories` (shipped:
|
|
50
|
+
* `specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
51
|
+
*
|
|
52
|
+
* The endpoint had shipped for a while; what this bridge lacked was a way to
|
|
53
|
+
* CALL it — `support/specguard-api.ts` offered only `getJson`, which hardcoded
|
|
54
|
+
* `method: "GET"` and took no body. The reservation recorded here was that the
|
|
55
|
+
* write transport should land WITH the first write tool, designed against a real
|
|
56
|
+
* request body and a real 4xx surface rather than invented for a caller that did
|
|
57
|
+
* not exist. That is what happened: `postJson`/`postJsonObject` arrived with
|
|
58
|
+
* this entry, sharing `fetchWithTimeout` with the read path rather than standing
|
|
59
|
+
* beside it, and `describeFailure` grew the `400` branch that surfaces
|
|
60
|
+
* SpecGuard's own refusal sentence — the modal answer this endpoint gives.
|
|
61
|
+
*
|
|
62
|
+
* The standing rule is unchanged and still binding, which is what keeps the rest
|
|
63
|
+
* of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
|
|
64
|
+
* the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
|
|
65
|
+
* wrapped here however useful a tool for them would be. What moved was the
|
|
66
|
+
* platform, not the bar.
|
|
53
67
|
*/
|
|
54
68
|
export const TOOLS = [
|
|
55
69
|
lintIntentAnnotations,
|
|
56
70
|
getRepositoryOverview,
|
|
57
71
|
listRepositories,
|
|
72
|
+
addRepository,
|
|
58
73
|
];
|
|
59
74
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAG7D
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;CACd,CAAC"}
|
|
@@ -13,13 +13,20 @@ import type { ToolDefinition } from "./types.js";
|
|
|
13
13
|
* answer, and it is the whole of what this tool does.
|
|
14
14
|
*
|
|
15
15
|
* It is also the tool that proves the second credential slot works end to end,
|
|
16
|
-
* which is why it
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* which is why it shipped alone. It no longer IS alone: `add_repository`
|
|
17
|
+
* (SPGD-764) reads the same `sgu_` key and wraps `POST /api/v1/repositories`,
|
|
18
|
+
* which arrived once this bridge had a write transport to call it with —
|
|
19
|
+
* `postJson`/`postJsonObject` in `support/specguard-api.ts`, landed with that
|
|
20
|
+
* tool and designed against a real request body and a real 4xx surface, exactly
|
|
21
|
+
* as the reservation recorded here asked. The registry's standing rule
|
|
22
|
+
* (`tools/index.ts`) is unchanged and still binding: a tool in `tools/list` is a
|
|
23
|
+
* promise an agent acts on, so the REST of the user-scoped surface — removing a
|
|
24
|
+
* repository, minting or revoking keys — stays out until those endpoints ship.
|
|
25
|
+
*
|
|
26
|
+
* What this tool still uniquely answers is the question above: which
|
|
27
|
+
* repositories there ARE. `add_repository` extends that surface rather than
|
|
28
|
+
* replacing it, and the two share a handle — the `full_name` reported here is
|
|
29
|
+
* the `full_name` that one takes.
|
|
23
30
|
*
|
|
24
31
|
* == It reads the OTHER key, and that is the point
|
|
25
32
|
*
|
|
@@ -13,13 +13,20 @@ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js
|
|
|
13
13
|
* answer, and it is the whole of what this tool does.
|
|
14
14
|
*
|
|
15
15
|
* It is also the tool that proves the second credential slot works end to end,
|
|
16
|
-
* which is why it
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* which is why it shipped alone. It no longer IS alone: `add_repository`
|
|
17
|
+
* (SPGD-764) reads the same `sgu_` key and wraps `POST /api/v1/repositories`,
|
|
18
|
+
* which arrived once this bridge had a write transport to call it with —
|
|
19
|
+
* `postJson`/`postJsonObject` in `support/specguard-api.ts`, landed with that
|
|
20
|
+
* tool and designed against a real request body and a real 4xx surface, exactly
|
|
21
|
+
* as the reservation recorded here asked. The registry's standing rule
|
|
22
|
+
* (`tools/index.ts`) is unchanged and still binding: a tool in `tools/list` is a
|
|
23
|
+
* promise an agent acts on, so the REST of the user-scoped surface — removing a
|
|
24
|
+
* repository, minting or revoking keys — stays out until those endpoints ship.
|
|
25
|
+
*
|
|
26
|
+
* What this tool still uniquely answers is the question above: which
|
|
27
|
+
* repositories there ARE. `add_repository` extends that surface rather than
|
|
28
|
+
* replacing it, and the two share a handle — the `full_name` reported here is
|
|
29
|
+
* the `full_name` that one takes.
|
|
23
30
|
*
|
|
24
31
|
* == It reads the OTHER key, and that is the point
|
|
25
32
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF
|
|
1
|
+
{"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AACH,MAAM,gBAAgB,GAAmB;IACvC,IAAI,EAAE,mBAAmB;IACzB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EACT,2FAA2F;QAC3F,2FAA2F;QAC3F,6DAA6D;QAC7D,6FAA6F;QAC7F,wDAAwD;QACxD,mFAAmF;QACnF,2FAA2F;QAC3F,uDAAuD;QACvD,kEAAkE;QAClE,6FAA6F;QAC7F,8FAA8F;QAC9F,wFAAwF;QACxF,4FAA4F;QAC5F,QAAQ;IACV,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,oEAAoE;QACpE,wEAAwE;QACxE,sEAAsE;QACtE,2EAA2E;QAC3E,uEAAuE;QACvE,uCAAuC;QACvC,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;QACtB,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,sBAAsB,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAEpF,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,gBAAgB,CAAC"}
|
|
@@ -449,9 +449,17 @@ const getRepositoryOverview = {
|
|
|
449
449
|
"branch's: pass `commit_sha` to be answered about ONE named run instead — after pushing a " +
|
|
450
450
|
"commit and waiting for CI, say — then read `run_anchor` to confirm which run you were served, " +
|
|
451
451
|
"because an unknown sha falls back to the newest rather than erroring. " +
|
|
452
|
-
"Pass `unannotated_examples: true` to list the individual tests
|
|
452
|
+
"Pass `unannotated_examples: true` to list the individual tests carrying no `@intent` — the " +
|
|
453
453
|
"examples behind the annotated ratio, which is otherwise a percentage with nothing to act on — " +
|
|
454
454
|
"and, in the same answer, which AREAS of the suite carry the most of them. " +
|
|
455
|
+
"HOW MUCH OF THE SUITE SPECGUARD CAN READ IS A DIFFERENT QUESTION FROM HOW MUCH IS ANNOTATED, " +
|
|
456
|
+
"and `latest_run.intent_readings` answers it on every response with no flag to pass. A test " +
|
|
457
|
+
"written in the ordinary `Class#method behavior` shape yields an entity, an action and a " +
|
|
458
|
+
"behavior from its own description, so SpecGuard reads it even with no annotation: " +
|
|
459
|
+
"`intent_readings.derived` counts those, `authored` counts the annotated ones, and `unreadable` " +
|
|
460
|
+
"is the ONLY figure on this endpoint that means tests SpecGuard can say nothing about. Never " +
|
|
461
|
+
"read `total_specs - annotated_specs` as that population — on a suite that has never been " +
|
|
462
|
+
"annotated it is the whole suite, and almost all of it is readable. " +
|
|
455
463
|
"TWO BLOCKS COME BACK ON EVERY RESPONSE — no parameter to pass, no flag to set — and they " +
|
|
456
464
|
"answer what everything above silently depends on: is SpecGuard still being fed? " +
|
|
457
465
|
"`delivery_health` is why the figures may be STALE: `refusing` is a comparison of stamps, not a " +
|
|
@@ -667,10 +675,22 @@ const getRepositoryOverview = {
|
|
|
667
675
|
unannotated_examples: {
|
|
668
676
|
type: "boolean",
|
|
669
677
|
description: "Open a run's UNANNOTATED examples — the individual tests behind `latest_run`'s " +
|
|
670
|
-
"`total_specs` MINUS `annotated_specs
|
|
671
|
-
"\"SpecGuard cannot see the other N tests\". Every other population this endpoint reports " +
|
|
678
|
+
"`total_specs` MINUS `annotated_specs`. Every other population this endpoint reports " +
|
|
672
679
|
"can be walked down to the examples it counts; annotation coverage was the exception, so " +
|
|
673
680
|
"`annotated_ratio` told you how far you had to go and not one test to annotate. " +
|
|
681
|
+
"UNANNOTATED IS NOT THE SAME AS UNREADABLE, and this block reports both. Every row here " +
|
|
682
|
+
"lacks an `@intent`, which is exact — and most of them SpecGuard READS anyway, from the " +
|
|
683
|
+
"test's own description. Each row carries `reading` (`\"derived\"` or `\"unreadable\"`) and " +
|
|
684
|
+
"`derived_intent` (the `entity`/`action`/`behavior` it got, or `null`), and the block " +
|
|
685
|
+
"carries `derived_count` and `unreadable_count` beside `recorded_count`. Only " +
|
|
686
|
+
"`unreadable_count` is a count of tests SpecGuard can say nothing about; treat the rest " +
|
|
687
|
+
"as annotation debt, not as blindness. THE UNREADABLE ROWS COME FIRST in the list, ahead " +
|
|
688
|
+
"of the derived ones, so the cap cannot hide them. " +
|
|
689
|
+
"A derived reading is WEAKER than an authored one and must never be presented as " +
|
|
690
|
+
"equivalent: it carries no preconditions, its behavior is prose somebody wrote for a test " +
|
|
691
|
+
"runner's output rather than a declared statement of what the test is for, and its layer " +
|
|
692
|
+
"is inferred from the directory rather than declared — which is why `derived_intent` " +
|
|
693
|
+
"carries no `layer` key at all. " +
|
|
674
694
|
"THIS ONE IS A FLAG, NOT A NAME — the only argument here that takes `true` rather than a " +
|
|
675
695
|
"value. The others open the rows behind a LINE of a ranking and so carry that line's key; " +
|
|
676
696
|
"this opens a POPULATION, which is a subtraction on the run and has no line to name. " +
|
|
@@ -681,8 +701,9 @@ const getRepositoryOverview = {
|
|
|
681
701
|
"one is additional, not instead. " +
|
|
682
702
|
"Asking populates `latest_run.unannotated_examples` — up to 100 of the unannotated " +
|
|
683
703
|
"examples OF WHATEVER YOU ASKED FOR, each with `name`, `file_path`, `line_number` and " +
|
|
684
|
-
"`spec_file_path` (
|
|
685
|
-
"per-example drill-ins above), plus that same population's own
|
|
704
|
+
"`spec_file_path`, `reading` and `derived_intent` (SIX fields: no `duration_seconds` and " +
|
|
705
|
+
"no `outcome`, unlike the per-example drill-ins above), plus that same population's own " +
|
|
706
|
+
"`recorded_count`, `derived_count` and `unreadable_count`, the " +
|
|
686
707
|
"`limit` the row list was cut at, and `spec_file`/`spec_directory` ECHOED BACK as the " +
|
|
687
708
|
"server READ them — `null` for each one you did not send. " +
|
|
688
709
|
"READ THE ECHO BEFORE YOU READ THE COUNT. `unannotated_examples.recorded_count` — the " +
|
|
@@ -700,13 +721,17 @@ const getRepositoryOverview = {
|
|
|
700
721
|
"THE DEBT IS — one run's annotation debt rolled up by code AREA, which is what you pick " +
|
|
701
722
|
"the next `spec_directory` narrowing FROM. Both come from this ONE flag: there is no " +
|
|
702
723
|
"second parameter to send and no new value. " +
|
|
703
|
-
"The map's rows carry `path`, `unannotated_count
|
|
704
|
-
"counted against
|
|
724
|
+
"The map's rows carry `path`, `unannotated_count`, the `recorded_count` that area was " +
|
|
725
|
+
"counted against, and the same three-way split as the worklist — `authored_count`, " +
|
|
726
|
+
"`derived_count` and `unreadable_count`, which sum to `recorded_count` while the last two " +
|
|
727
|
+
"sum to `unannotated_count` (the operands, never a fraction), plus `directory_count` — EVERY area " +
|
|
705
728
|
"the run touched, not every area with debt, and not `rows.size` — and its OWN `limit`, " +
|
|
706
729
|
"which is 10 and NOT the worklist's 100. Two caps under one ask, and the difference is " +
|
|
707
730
|
"the kind of list: 100 caps a WORKLIST to work through, 10 caps a RANKING to pick from. " +
|
|
708
731
|
"The orders differ for the same reason — the worklist is file-navigable, the map is " +
|
|
709
|
-
"ranked `unannotated_count` DESC with `path` as a tiebreak
|
|
732
|
+
"ranked `unreadable_count` DESC, then `unannotated_count` DESC, with `path` as a tiebreak " +
|
|
733
|
+
"only — the areas SpecGuard cannot read lead, because a ten-row ranking led by debt on an " +
|
|
734
|
+
"unannotated suite is a ranking by area SIZE and the dark corners never surface. A fully-annotated area " +
|
|
710
735
|
"is a real ROW with `unannotated_count: 0`, never an omission. Those rows sort last " +
|
|
711
736
|
"COLLECTIVELY, so on a run with more areas than the cap they are cut and never seen, " +
|
|
712
737
|
"but on a run inside the cap they ARE LISTED and listed is correct. So `rows.size` is " +
|