focused-spec 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.
@@ -0,0 +1,107 @@
1
+ import { parseEvidenceReference, STABLE_ID } from './parser.js';
2
+ import { loadActiveChanges, loadChangeDocuments, loadCurrentDocuments, validateSourceSelection } from './sources.js';
3
+ function diagnostic(scenario, message) {
4
+ return {
5
+ path: scenario.path,
6
+ line: scenario.line,
7
+ ...(scenario.ids[0] === undefined ? {} : { scenarioId: scenario.ids[0] }),
8
+ message,
9
+ };
10
+ }
11
+ export function validateDocuments(documents, config, options) {
12
+ const violations = [];
13
+ const identifiers = new Map();
14
+ for (const document of documents) {
15
+ for (const line of document.malformedScenarioHeadings) {
16
+ violations.push({ path: document.path, line, message: 'scenario headings must use exactly four hashes' });
17
+ }
18
+ for (const scenario of document.scenarios) {
19
+ if (scenario.ids.length !== 1) {
20
+ violations.push(diagnostic(scenario, `expected exactly one ID row, found ${scenario.ids.length}`));
21
+ }
22
+ else {
23
+ const id = scenario.ids[0];
24
+ if (!STABLE_ID.test(id))
25
+ violations.push(diagnostic(scenario, `invalid stable ID ${id}`));
26
+ const previous = identifiers.get(id);
27
+ if (previous === undefined)
28
+ identifiers.set(id, scenario);
29
+ else
30
+ violations.push(diagnostic(scenario, `duplicate stable ID; first owned by ${previous.path}:${previous.line}`));
31
+ }
32
+ if (scenario.evidence.length === 0)
33
+ violations.push(diagnostic(scenario, 'expected at least one EVIDENCE row'));
34
+ if (scenario.whenCount !== 1)
35
+ violations.push(diagnostic(scenario, `expected exactly one WHEN row, found ${scenario.whenCount}`));
36
+ if (scenario.thenCount !== 1)
37
+ violations.push(diagnostic(scenario, `expected exactly one THEN row, found ${scenario.thenCount}`));
38
+ for (const raw of scenario.evidence) {
39
+ const reference = parseEvidenceReference(raw);
40
+ if ('error' in reference) {
41
+ violations.push(diagnostic(scenario, `invalid evidence ${raw}: ${reference.error}`));
42
+ continue;
43
+ }
44
+ if (reference.planned && !options.allowPlanned) {
45
+ violations.push(diagnostic(scenario, `planned evidence is not allowed here: ${raw}; use non-strict validate --change <name> while planning, then replace planned evidence before strict validation or run`));
46
+ }
47
+ if (config.runners[reference.runnerId] === undefined) {
48
+ violations.push(diagnostic(scenario, `unknown evidence runner ${reference.runnerId}`));
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return { violations, identifiers };
54
+ }
55
+ function collisionViolations(current, documents) {
56
+ const violations = [];
57
+ for (const document of documents) {
58
+ for (const scenario of document.scenarios) {
59
+ const id = scenario.ids[0];
60
+ const owner = id === undefined ? undefined : current.get(id);
61
+ if (owner === undefined || scenario.operation === 'MODIFIED' || scenario.operation === 'REMOVED')
62
+ continue;
63
+ violations.push(diagnostic(scenario, `stable ID already belongs to current scenario ${owner.path}:${owner.line}`));
64
+ }
65
+ }
66
+ return violations;
67
+ }
68
+ export async function validateFocusedSpecs(projectRoot, config, options = {}) {
69
+ const violations = validateSourceSelection(config, options.changeName);
70
+ const currentDocuments = await loadCurrentDocuments(projectRoot, config);
71
+ const current = validateDocuments(currentDocuments, config, { allowPlanned: false });
72
+ violations.push(...current.violations);
73
+ if (options.changeName !== undefined) {
74
+ const changeDocuments = await loadChangeDocuments(projectRoot, config, options.changeName);
75
+ if (changeDocuments.length === 0) {
76
+ violations.push({ path: options.changeName, message: `OpenSpec change has no delta specifications: ${options.changeName}` });
77
+ }
78
+ const active = validateDocuments(changeDocuments, config, { allowPlanned: !options.strict });
79
+ violations.push(...active.violations, ...collisionViolations(current.identifiers, changeDocuments));
80
+ return {
81
+ violations: deduplicate(violations),
82
+ targetDocuments: changeDocuments,
83
+ resolutionDocuments: [...currentDocuments, ...changeDocuments],
84
+ };
85
+ }
86
+ const resolutionDocuments = [...currentDocuments];
87
+ if (config.specifications.source === 'openspec') {
88
+ for (const documents of (await loadActiveChanges(projectRoot, config)).values()) {
89
+ const active = validateDocuments(documents, config, { allowPlanned: true });
90
+ violations.push(...active.violations, ...collisionViolations(current.identifiers, documents));
91
+ resolutionDocuments.push(...documents);
92
+ }
93
+ }
94
+ return { violations: deduplicate(violations), targetDocuments: currentDocuments, resolutionDocuments };
95
+ }
96
+ function deduplicate(violations) {
97
+ const byKey = new Map();
98
+ for (const violation of violations) {
99
+ const key = `${violation.path}:${violation.line ?? ''}:${violation.scenarioId ?? ''}:${violation.message}`;
100
+ byKey.set(key, violation);
101
+ }
102
+ return [...byKey.values()].sort((left, right) => {
103
+ const path = left.path.localeCompare(right.path);
104
+ return path !== 0 ? path : (left.line ?? 0) - (right.line ?? 0);
105
+ });
106
+ }
107
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAEpH,SAAS,UAAU,CAAC,QAAkB,EAAE,OAAe;IACrD,OAAO;QACL,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,OAAO;KACR,CAAA;AACH,CAAC;AAOD,MAAM,UAAU,iBAAiB,CAC/B,SAAkC,EAClC,MAAyB,EACzB,OAA2C;IAE3C,MAAM,UAAU,GAAgB,EAAE,CAAA;IAClC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAoB,CAAA;IAE/C,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,yBAAyB,EAAE,CAAC;YACtD,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gDAAgD,EAAE,CAAC,CAAA;QAC3G,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;YAC1C,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC9B,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,sCAAsC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YACpG,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAW,CAAA;gBACpC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;oBAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAC,CAAA;gBACzF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBACpC,IAAI,QAAQ,KAAK,SAAS;oBAAE,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;;oBACpD,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,uCAAuC,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;YACrH,CAAC;YACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,oCAAoC,CAAC,CAAC,CAAA;YAC/G,IAAI,QAAQ,CAAC,SAAS,KAAK,CAAC;gBAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,wCAAwC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;YACjI,IAAI,QAAQ,CAAC,SAAS,KAAK,CAAC;gBAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,wCAAwC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;YAEjI,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACpC,MAAM,SAAS,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAA;gBAC7C,IAAI,OAAO,IAAI,SAAS,EAAE,CAAC;oBACzB,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,oBAAoB,GAAG,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;oBACpF,SAAQ;gBACV,CAAC;gBACD,IAAI,SAAS,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;oBAC/C,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,yCAAyC,GAAG,yHAAyH,CAAC,CAAC,CAAA;gBAC9M,CAAC;gBACD,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;oBACrD,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,2BAA2B,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;gBACxF,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,CAAA;AACpC,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAsC,EAAE,SAAkC;IACrG,MAAM,UAAU,GAAgB,EAAE,CAAA;IAClC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;YAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YAC1B,MAAM,KAAK,GAAG,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC5D,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,KAAK,UAAU,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS;gBAAE,SAAQ;YAC1G,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,iDAAiD,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QACpH,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,WAAmB,EACnB,MAAyB,EACzB,UAA6B,EAAE;IAE/B,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;IACtE,MAAM,gBAAgB,GAAG,MAAM,oBAAoB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;IACxE,MAAM,OAAO,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAA;IACpF,UAAU,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAEtC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,eAAe,GAAG,MAAM,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAC1F,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,gDAAgD,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;QAC9H,CAAC;QACD,MAAM,MAAM,GAAG,iBAAiB,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;QAC5F,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC,CAAA;QACnG,OAAO;YACL,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC;YACnC,eAAe,EAAE,eAAe;YAChC,mBAAmB,EAAE,CAAC,GAAG,gBAAgB,EAAE,GAAG,eAAe,CAAC;SAC/D,CAAA;IACH,CAAC;IAED,MAAM,mBAAmB,GAAG,CAAC,GAAG,gBAAgB,CAAC,CAAA;IACjD,IAAI,MAAM,CAAC,cAAc,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAChD,KAAK,MAAM,SAAS,IAAI,CAAC,MAAM,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YAChF,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3E,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAA;YAC7F,mBAAmB,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,EAAE,eAAe,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAA;AACxG,CAAC;AAED,SAAS,WAAW,CAAC,UAAgC;IACnD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAA;IAC1C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,IAAI,EAAE,IAAI,SAAS,CAAC,UAAU,IAAI,EAAE,IAAI,SAAS,CAAC,OAAO,EAAE,CAAA;QAC1G,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;IAC3B,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAChD,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;AACJ,CAAC"}
package/install.sh ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PACKAGE_NAME="${FOCUSED_SPEC_PACKAGE:-focused-spec}"
5
+ NPM_BIN="${NPM_BIN:-npm}"
6
+
7
+ if ! command -v node >/dev/null 2>&1; then
8
+ printf '%s\n' 'focused-spec requires Node.js 24 or newer; node was not found.' >&2
9
+ exit 1
10
+ fi
11
+
12
+ node_major="$(node -p 'Number(process.versions.node.split(".")[0])')"
13
+ if [[ "$node_major" -lt 24 ]]; then
14
+ printf 'focused-spec requires Node.js 24 or newer; found %s.\n' "$(node --version)" >&2
15
+ exit 1
16
+ fi
17
+
18
+ if ! command -v "$NPM_BIN" >/dev/null 2>&1; then
19
+ printf 'npm executable not found: %s\n' "$NPM_BIN" >&2
20
+ exit 1
21
+ fi
22
+
23
+ printf 'Installing %s as a development dependency...\n' "$PACKAGE_NAME"
24
+ "$NPM_BIN" install --save-dev "$PACKAGE_NAME"
25
+
26
+
27
+ printf '%s\n' 'focused-spec CLI installation complete.'
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "focused-spec",
3
+ "version": "0.1.0",
4
+ "description": "Executable evidence for focused behavioral specifications",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/bamanoz/focused-spec.git"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "focused-spec": "dist/cli.js"
12
+ },
13
+ "exports": {
14
+ ".": "./dist/index.js",
15
+ "./runner": "./dist/runner-api.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "skills",
20
+ "install.sh",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "prepare": "npm run build",
27
+ "typecheck:tests": "tsc -p test/tsconfig.json && tsc -p examples/polyglot/tsconfig.json && tsc -p evals/tsconfig.json",
28
+ "test": "npm run build && npm run typecheck:tests && vitest run",
29
+ "check": "npm test",
30
+ "smoke": "node dist/cli.js run --root examples/polyglot",
31
+ "eval:agent": "node --experimental-strip-types evals/run.ts"
32
+ },
33
+ "engines": {
34
+ "node": ">=24"
35
+ },
36
+ "dependencies": {
37
+ "fast-glob": "^3.3.3",
38
+ "yaml": "^2.8.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.10.1",
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^4.0.8"
44
+ },
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: focused-spec
3
+ description: MUST be used alongside any workflow that creates or changes behavioral specifications or scenarios, even when another skill owns that workflow. Enforces focused scenario IDs, executable evidence, on-demand configuration, and project-local runners.
4
+ ---
5
+
6
+ # Focused Spec
7
+
8
+ Any behavioral specification authored in a project is a focused specification. Keep the surrounding framework's document structure, but write every behavioral scenario in the format below.
9
+
10
+ ## Author a scenario
11
+
12
+ 1. Find the single capability that owns the behavior; do not create a parallel formulation.
13
+ 2. Protect the smallest product-boundary outcome that can fail independently.
14
+ 3. Give the scenario one repository-unique lowercase dotted `ID` and preserve it when ownership moves.
15
+ 4. Determine the execution environment, runner ID, and opaque selector shape before writing evidence.
16
+ 5. Add one or more `EVIDENCE` rows. Multiple rows form an AND contract.
17
+ 6. Write exactly one `WHEN` request and one independently failing `THEN` outcome.
18
+
19
+ ```markdown
20
+ #### Scenario: Blocked account submits valid credentials
21
+ - **ID**: `auth.login.blocked-account`
22
+ - **EVIDENCE**: `go-unit::./internal/auth::TestBlockedAccount`
23
+ - **EVIDENCE**: `pytest-functional::tests/test_auth.py::test_blocked_account`
24
+ - **WHEN** a blocked account submits otherwise valid credentials
25
+ - **THEN** authentication is rejected
26
+ ```
27
+
28
+ Evidence is `[planned:]<runner-id>::<opaque selector>`. Only the first `::` separates the runner ID. Use `planned:` only while that exact test target does not exist; the runner ID, module path, and selector contract must already be chosen. Remove every `planned:` before completing implementation.
29
+
30
+ ## Configure on demand
31
+
32
+ Create or update `.focused-spec/config.yaml` as soon as the first scenario names evidence. Keep runner modules in `.focused-spec/runners/`. Do not create root-level `focused-spec.yaml`.
33
+
34
+ For Markdown files selected by glob:
35
+
36
+ ```yaml
37
+ version: 1
38
+ specifications:
39
+ source: files
40
+ paths: [specs/**/*.md]
41
+ runners:
42
+ project-tests:
43
+ module: ./.focused-spec/runners/project-tests.ts
44
+ timeoutMs: 120000
45
+ ```
46
+
47
+ For a framework-managed source supported by the CLI, select it instead; for example:
48
+
49
+ ```yaml
50
+ specifications:
51
+ source: openspec
52
+ root: openspec # optional; defaults to openspec
53
+ ```
54
+
55
+ `runners` is a map keyed by runner ID. Each entry requires `module`; optional fields are project-relative `cwd`, positive integer `timeoutMs`, and JSON-compatible `options`. `module` must be a project-contained `.ts`, `.mts`, `.js`, or `.mjs` file. Every runner ID referenced by evidence must be registered. For another specification framework, use `source: files` with paths targeting its Markdown scenario files unless the CLI supports a dedicated source.
56
+
57
+ ## Implement a runner
58
+
59
+ Import types only from `focused-spec/runner`. A default-exported `RunnerPlugin` has `apiVersion: 1`, `resolve(request)`, and `run(request)`.
60
+
61
+ - `resolve` receives `selectors`, `projectRoot`, resolved `cwd`, `runnerId`, JSON-compatible `options`, and abort `signal`.
62
+ - For every selector, `resolve` returns exactly one target `{ selector, targetId, displayName, source?, data? }` or one error `{ selector, message }`.
63
+ - Prove target existence through the real framework's collection/listing mechanism. Preserve the selector exactly and use deterministic target IDs.
64
+ - `run` receives resolved `targets` plus the same context and returns exactly one `{ targetId, status, diagnostic? }` per target.
65
+ - Status is `pass`, `fail`, or `skip`. Never return `pass` without executing and interpreting the selected test.
66
+ - Spawn tools with executable/argument arrays, `shell: false`, supplied `cwd`, and supplied `signal`. Keep diagnostics bounded.
67
+
68
+ The public shape is:
69
+
70
+ ```ts
71
+ interface RunnerPlugin {
72
+ readonly apiVersion: 1
73
+ resolve(request: ResolveRequest): Promise<{ targets: ResolvedTarget[]; errors: ResolveError[] }>
74
+ run(request: RunRequest): Promise<{ results: TargetResult[] }>
75
+ }
76
+ ```
77
+
78
+ ## Verify
79
+
80
+ While a named change still contains `planned:` evidence, validate its structure with `focused-spec validate --change <name> --syntax-only` and its configuration with `focused-spec validate --change <name>` (without `--strict`). Planned targets are not resolved or executed; a successful planning check does not prove them.
81
+
82
+ Only after implementing the tests and runner, replace all `planned:` references with concrete selectors. Then verify the completed change:
83
+
84
+ ```sh
85
+ focused-spec validate --change <name> --strict
86
+ focused-spec run --change <name>
87
+ ```
88
+
89
+ For current specifications (no change):
90
+
91
+ ```sh
92
+ focused-spec validate
93
+ focused-spec run
94
+ ```
95
+
96
+ Do not run `--strict` or `run` against a change that intentionally still contains `planned:`; their failure is expected, not a proposal defect. Full validation resolves non-planned evidence; `run` performs strict validation and executes it. `SKIP` and `ERROR` are not success. Use `--allow-skip` only when explicit project policy permits unavailable optional evidence.