@specpin/cli 0.0.1 → 0.0.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 +15 -0
- package/package.json +6 -2
- package/scripts/sync-skill.mjs +80 -0
- package/skill/SKILL.md +139 -0
- package/skill/references/cli-commands.md +73 -0
- package/skill/references/fingerprint-strategy.md +80 -0
- package/skill/references/schema-authoring.md +147 -0
package/README.md
CHANGED
|
@@ -34,6 +34,21 @@ specpin validate # lint the spec corpus (CI-friendly)
|
|
|
34
34
|
specpin --help
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Author specs with a coding agent
|
|
38
|
+
|
|
39
|
+
This package bundles a portable skill that teaches a coding agent (Claude Code,
|
|
40
|
+
Cursor, etc.) to author schema-valid specs and drive the CLI. The CLI adds no
|
|
41
|
+
LLM; the host agent is the author. It ships in the tarball and is reachable
|
|
42
|
+
without installing:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
https://unpkg.com/@specpin/cli@latest/skill/SKILL.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Point your agent at that URL (or the installed `skill/SKILL.md`), then it writes
|
|
49
|
+
`.specs/` and runs `specpin validate`. See
|
|
50
|
+
[`docs/ai-authoring.md`](https://github.com/lamngockhuong/specpin/blob/main/docs/ai-authoring.md).
|
|
51
|
+
|
|
37
52
|
## How it works
|
|
38
53
|
|
|
39
54
|
- The npm package version matches the CLI release version. Postinstall fetches
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specpin/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Specpin sidecar CLI: init + serve the .specs/ knowledge layer for the Specpin browser extension. Installs the matching prebuilt Go binary.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specpin",
|
|
@@ -25,11 +25,15 @@
|
|
|
25
25
|
"node": ">=20"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"postinstall": "node scripts/postinstall.mjs"
|
|
28
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
29
|
+
"sync-skill": "node scripts/sync-skill.mjs",
|
|
30
|
+
"check-skill": "node scripts/sync-skill.mjs --check",
|
|
31
|
+
"prepack": "node scripts/sync-skill.mjs"
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"bin",
|
|
32
35
|
"scripts",
|
|
36
|
+
"skill",
|
|
33
37
|
"README.md"
|
|
34
38
|
],
|
|
35
39
|
"publishConfig": {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Sync the canonical skill (apps/cli/skill/) into the npm package
|
|
2
|
+
// (apps/cli/npm/skill/) so it ships in the published tarball and is reachable
|
|
3
|
+
// via unpkg/jsdelivr. This mirrors how `make sync-schema` copies the SSOT
|
|
4
|
+
// schema into the Go embed location: one source of truth, a checked-in copy,
|
|
5
|
+
// and a drift gate.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/sync-skill.mjs copy source -> bundled (default)
|
|
8
|
+
// node scripts/sync-skill.mjs --check exit non-zero if they differ (CI gate)
|
|
9
|
+
//
|
|
10
|
+
// npm tarballs do not reliably follow symlinks across environments, so the
|
|
11
|
+
// bundled copy is a real checked-in directory kept honest by --check.
|
|
12
|
+
|
|
13
|
+
import { readdir, readFile, cp, rm } from "node:fs/promises";
|
|
14
|
+
import { dirname, join, relative } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SRC = join(here, "..", "..", "skill"); // apps/cli/skill
|
|
19
|
+
const DST = join(here, "..", "skill"); // apps/cli/npm/skill
|
|
20
|
+
|
|
21
|
+
// Recursively list files (relative paths) under a directory. Returns [] if the
|
|
22
|
+
// directory is absent rather than throwing, so --check can report a clean miss.
|
|
23
|
+
async function listFiles(root) {
|
|
24
|
+
const out = [];
|
|
25
|
+
async function walk(dir) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code === "ENOENT") return;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
const abs = join(dir, e.name);
|
|
35
|
+
if (e.isDirectory()) await walk(abs);
|
|
36
|
+
else out.push(relative(root, abs));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await walk(root);
|
|
40
|
+
return out.sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function copy() {
|
|
44
|
+
const files = await listFiles(SRC);
|
|
45
|
+
if (files.length === 0) {
|
|
46
|
+
console.error(`sync-skill: no files under ${SRC}; nothing to sync`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// Clear the destination so deletions in source propagate, then copy fresh.
|
|
50
|
+
await rm(DST, { recursive: true, force: true });
|
|
51
|
+
await cp(SRC, DST, { recursive: true });
|
|
52
|
+
console.log(`sync-skill: copied ${files.length} file(s) -> ${relative(process.cwd(), DST)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function check() {
|
|
56
|
+
const [srcFiles, dstFiles] = await Promise.all([listFiles(SRC), listFiles(DST)]);
|
|
57
|
+
const offending = [];
|
|
58
|
+
|
|
59
|
+
const srcSet = new Set(srcFiles);
|
|
60
|
+
const dstSet = new Set(dstFiles);
|
|
61
|
+
for (const rel of srcFiles) if (!dstSet.has(rel)) offending.push(`missing in bundle: ${rel}`);
|
|
62
|
+
for (const rel of dstFiles) if (!srcSet.has(rel)) offending.push(`stale in bundle: ${rel}`);
|
|
63
|
+
|
|
64
|
+
for (const rel of srcFiles) {
|
|
65
|
+
if (!dstSet.has(rel)) continue;
|
|
66
|
+
const [a, b] = await Promise.all([readFile(join(SRC, rel)), readFile(join(DST, rel))]);
|
|
67
|
+
if (!a.equals(b)) offending.push(`content differs: ${rel}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (offending.length > 0) {
|
|
71
|
+
console.error("sync-skill: bundled skill drifted from apps/cli/skill/:");
|
|
72
|
+
for (const o of offending) console.error(` ${o}`);
|
|
73
|
+
console.error("Run `npm run sync-skill` in apps/cli/npm to re-sync.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log("sync-skill: bundled skill in sync");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.argv.includes("--check")) await check();
|
|
80
|
+
else await copy();
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specpin
|
|
3
|
+
description: >-
|
|
4
|
+
Author and pin living business specs (rules, descriptions, acceptance
|
|
5
|
+
criteria) onto the elements of a running web UI, and drive the specpin
|
|
6
|
+
sidecar CLI. Use when the user wants to document UI elements with versioned
|
|
7
|
+
specs, write or edit .specs/*.spec.json files, run "specpin serve"/"specpin
|
|
8
|
+
validate", or mentions specpin, spec.json, or pinning specs to a page. The
|
|
9
|
+
host coding agent is the author: it reads the UI source, writes schema-valid
|
|
10
|
+
JSON, registers it in the manifest, and validates. No application code is
|
|
11
|
+
generated.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Specpin: author business specs for a running web UI
|
|
15
|
+
|
|
16
|
+
Specpin is a Git-native knowledge layer. It pins business specifications onto
|
|
17
|
+
the elements of a web UI you already have, then renders them in the browser as
|
|
18
|
+
you hover or browse. It is NOT a code generator and does not write application
|
|
19
|
+
code. You (the coding agent) author JSON spec files in the consumer repo's
|
|
20
|
+
`.specs/` directory; the `specpin` CLI serves and validates them; a browser
|
|
21
|
+
extension matches each spec's element fingerprint against the live DOM and
|
|
22
|
+
renders it.
|
|
23
|
+
|
|
24
|
+
Flow: `.specs/` (in the repo) -> `specpin serve` (Go sidecar, localhost) ->
|
|
25
|
+
browser extension (match + render).
|
|
26
|
+
|
|
27
|
+
Your job: read the UI source, write `<area>.spec.json` files that conform to
|
|
28
|
+
schema v1, register them in `manifest.json`, and confirm `specpin validate`
|
|
29
|
+
exits 0.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
No login, no API key, no model. The sidecar is localhost-only and prints its own
|
|
34
|
+
bearer token on `serve`. Contrast with agent CLIs that need auth: there is
|
|
35
|
+
nothing to authenticate here.
|
|
36
|
+
|
|
37
|
+
1. Check the CLI: `specpin --version` (or `npx @specpin/cli --version`).
|
|
38
|
+
2. If absent, install: `npm i -g @specpin/cli` (or use `npx @specpin/cli ...`).
|
|
39
|
+
3. Work inside the consumer repo. Author into its `.specs/` directory, never a
|
|
40
|
+
temp directory.
|
|
41
|
+
|
|
42
|
+
## Authoring workflow (the core loop)
|
|
43
|
+
|
|
44
|
+
a. **Scaffold** (only if `.specs/manifest.json` is absent):
|
|
45
|
+
`specpin init --project "<Name>" --domains <origin>` where `<origin>` is
|
|
46
|
+
where the UI runs, e.g. `localhost:3000`. This refuses to overwrite an
|
|
47
|
+
existing manifest.
|
|
48
|
+
|
|
49
|
+
b. **Identify the target element** in the UI source (JSX/HTML/Vue/Svelte) and
|
|
50
|
+
decide the fingerprint approach. Specpin is non-intrusive by default: build the
|
|
51
|
+
fingerprint from signals the element **already has** (an existing
|
|
52
|
+
`data-testid` / `data-spec-id`, a non-generated `id`, an `aria-label`, or a
|
|
53
|
+
unique selector) and do NOT edit the app's source. Adding a `data-spec-id` is
|
|
54
|
+
an optional opt-in upgrade for the most resilient anchor, only when the project
|
|
55
|
+
agrees to small markup additions. See `references/fingerprint-strategy.md`.
|
|
56
|
+
|
|
57
|
+
c. **Write `<area>.spec.json`** (one file per page or feature). It has a `group`
|
|
58
|
+
string and a `specs[]` array. Each spec needs:
|
|
59
|
+
- `id`: stable, unique within the project, e.g. `"login-submit-btn"`.
|
|
60
|
+
- `title`, `description`: **locale-keyed objects**, never flat strings,
|
|
61
|
+
e.g. `{ "en": "Log in button" }`. `description` must be non-empty.
|
|
62
|
+
- `businessRules` (optional): array of locale-keyed objects, one rule each.
|
|
63
|
+
- `tags` (optional): plain string array (not localized).
|
|
64
|
+
- `preferredDisplayMode` (optional): `tooltip` | `sidebar` | `modal`
|
|
65
|
+
(`overlay` and `inline-badge` are reserved and fall back to `tooltip`).
|
|
66
|
+
- `fingerprint`: the element link (required). See the fingerprint reference.
|
|
67
|
+
- `meta`: set `"source": "ai-generated"` for specs you author, plus
|
|
68
|
+
`createdBy`, `createdAt`, `updatedAt` (RFC3339).
|
|
69
|
+
|
|
70
|
+
`description` and each `businessRules` item may use a small, safe Markdown
|
|
71
|
+
subset (bold, italic, `[label](url)` links, and lists in `description`).
|
|
72
|
+
See `references/schema-authoring.md`.
|
|
73
|
+
|
|
74
|
+
d. **Register the file** in `manifest.json` under `specFiles[]`. A spec file on
|
|
75
|
+
disk that is not listed (or listed but missing) trips a drift warning.
|
|
76
|
+
|
|
77
|
+
e. **Validate**: `specpin validate` (or `specpin validate --dir <path>`).
|
|
78
|
+
Exit 0 = done. Exit 1 = fix the reported `FAIL` lines and re-run. Exit 2 =
|
|
79
|
+
the check could not run (no manifest, wrong `--dir`).
|
|
80
|
+
|
|
81
|
+
f. **Preview** (optional): `specpin serve` prints a URL + token to paste into the
|
|
82
|
+
extension, then specs render live on their elements with SSE live-reload.
|
|
83
|
+
|
|
84
|
+
## Do / Don't
|
|
85
|
+
|
|
86
|
+
DO:
|
|
87
|
+
- Set `meta.source: "ai-generated"` on every spec you author (output is meant to
|
|
88
|
+
be reviewed by a human).
|
|
89
|
+
- Keep one `<area>.spec.json` per page or feature; name it after the area.
|
|
90
|
+
- Prefer an anchor the element **already has** (an existing `data-testid` /
|
|
91
|
+
`data-spec-id`, a non-generated `id`, or an `aria-label`): it gives exact
|
|
92
|
+
matching with zero source changes.
|
|
93
|
+
- Ground every business rule in real code or stated requirements.
|
|
94
|
+
|
|
95
|
+
DON'T:
|
|
96
|
+
- Edit the app's source to add `data-spec-id` / `data-testid` unless the project
|
|
97
|
+
opts in. Specpin attaches to the UI you already have; adding an anchor is an
|
|
98
|
+
optional resilience upgrade, never a requirement. Default to synthesizing the
|
|
99
|
+
fingerprint from the existing markup.
|
|
100
|
+
- Invent business rules not supported by the code or the user's requirements.
|
|
101
|
+
- Use flat strings for `title` / `description` / `businessRules`: both
|
|
102
|
+
validators reject `"title": "Log in"`. Use `{ "en": "Log in" }`.
|
|
103
|
+
- Add keys not in the schema: objects are `additionalProperties: false`.
|
|
104
|
+
- Hand-edit generated artifacts or write files outside `.specs/`.
|
|
105
|
+
|
|
106
|
+
## Reading `specpin validate` output
|
|
107
|
+
|
|
108
|
+
Each file prints `OK <file>` or `FAIL <file>` followed by indented error lines,
|
|
109
|
+
then a summary `N files checked, M error(s)`. Drift between `manifest.specFiles`
|
|
110
|
+
and on-disk files prints `warning:` lines (or `FAIL:` under `--strict-manifest`).
|
|
111
|
+
|
|
112
|
+
- Exit 0: all valid. You are done.
|
|
113
|
+
- Exit 1: schema violations or an unreadable spec file. Read each `FAIL` block,
|
|
114
|
+
fix the JSON, re-run.
|
|
115
|
+
- Exit 2: could not run (missing `.specs/`, no `manifest.json`, or `--dir`
|
|
116
|
+
pointed at the repo root instead of `.specs/`). Fix the path or run `init`.
|
|
117
|
+
|
|
118
|
+
## Staying up to date
|
|
119
|
+
|
|
120
|
+
This skill ships inside `@specpin/cli` and is version-synced with the CLI. To
|
|
121
|
+
fetch the latest copy without installing:
|
|
122
|
+
|
|
123
|
+
- `https://unpkg.com/@specpin/cli@latest/skill/SKILL.md`
|
|
124
|
+
- `https://unpkg.com/@specpin/cli@latest/skill/references/<file>.md`
|
|
125
|
+
- Schema for `$schema` autocomplete: `https://specpin.ohnice.app/schema/v1.json`
|
|
126
|
+
(also `https://unpkg.com/@specpin/spec-schema/schema/v1.json`).
|
|
127
|
+
|
|
128
|
+
Compare `npm view @specpin/cli version` against your installed
|
|
129
|
+
`specpin --version`; refresh the skill if they differ.
|
|
130
|
+
|
|
131
|
+
## References (load on demand)
|
|
132
|
+
|
|
133
|
+
- `references/schema-authoring.md`: the full v1 shape (Manifest, SpecFile, Spec,
|
|
134
|
+
LocalizedString, fingerprint, meta), a complete minimal valid example, and the
|
|
135
|
+
Markdown subset.
|
|
136
|
+
- `references/fingerprint-strategy.md`: the test-id-first decision tree and how
|
|
137
|
+
to derive each fingerprint field from source.
|
|
138
|
+
- `references/cli-commands.md`: every command, its flags, and exit-code
|
|
139
|
+
semantics.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# CLI commands
|
|
2
|
+
|
|
3
|
+
Run `specpin <command>` (or `npx @specpin/cli <command>`). No auth or keys.
|
|
4
|
+
|
|
5
|
+
## init
|
|
6
|
+
|
|
7
|
+
Scaffold `.specs/manifest.json` in the current directory.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
specpin init --project "Acme CRM" --domains localhost:3000
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `--project` (required): manifest `project` name.
|
|
14
|
+
- `--domains` (optional): comma-separated origins, e.g. `localhost:3000,app.acme.io`.
|
|
15
|
+
- Writes a manifest with `$schema`, `version: "1.0"`, empty `specFiles`, and
|
|
16
|
+
default `settings` (`defaultLocale: en`, `matchConfidenceThreshold: 0.6`,
|
|
17
|
+
`defaultDisplayMode: tooltip`).
|
|
18
|
+
- Refuses to overwrite an existing `.specs/manifest.json` (exits with an error).
|
|
19
|
+
|
|
20
|
+
## serve
|
|
21
|
+
|
|
22
|
+
Serve `.specs/` over a hardened localhost HTTP API with SSE live-reload.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
specpin serve # auto-pick a free port
|
|
26
|
+
specpin serve --port 4317 # pin a port
|
|
27
|
+
specpin serve --dir path/to/.specs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `--port` (default 0 = auto-pick), `--dir` (default `.specs`).
|
|
31
|
+
- Prints the project name, dir, URL (`http://127.0.0.1:<port>`), and a bearer
|
|
32
|
+
token. Paste URL + token into the extension Options page.
|
|
33
|
+
- Binds `127.0.0.1` only; fails early if `manifest.json` is missing (usually
|
|
34
|
+
`--dir` pointed at the repo root instead of `.specs/`). Ctrl+C to stop.
|
|
35
|
+
|
|
36
|
+
## validate
|
|
37
|
+
|
|
38
|
+
Validate the corpus against the embedded schema, offline. CI-friendly.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
specpin validate # checks ./.specs
|
|
42
|
+
specpin validate --dir path/to/.specs
|
|
43
|
+
specpin validate --strict-manifest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `--dir` (default `.specs`), `--strict-manifest` (turn drift warnings into failures).
|
|
47
|
+
- Output: `OK <file>` or `FAIL <file>` plus indented errors per file, then
|
|
48
|
+
`N files checked, M error(s)`. Manifest/disk drift prints `warning:` lines
|
|
49
|
+
(or `FAIL:` under `--strict-manifest`).
|
|
50
|
+
- Exit codes:
|
|
51
|
+
- `0`: all valid.
|
|
52
|
+
- `1`: schema violations or an unreadable/symlinked spec file (author fixes).
|
|
53
|
+
- `2`: could not run (missing `.specs/`, no `manifest.json`, internal error).
|
|
54
|
+
|
|
55
|
+
## bundle
|
|
56
|
+
|
|
57
|
+
Assemble `.specs/` into a Manual-import bundle for the extension (no sidecar).
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
specpin bundle # prints JSON to stdout
|
|
61
|
+
specpin bundle --out out.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `--dir` (default `.specs`), `--out` (write to a file instead of stdout).
|
|
65
|
+
- Emits `{ "manifest": {...}, "files": { "<name>.spec.json": {...} } }`, pretty
|
|
66
|
+
printed. Paste into the extension's Manual specs import.
|
|
67
|
+
- Does NOT validate. Run `specpin validate` separately for schema checks.
|
|
68
|
+
|
|
69
|
+
## generate
|
|
70
|
+
|
|
71
|
+
Deferred stub. AI-assisted generation is driven by your coding agent through
|
|
72
|
+
this skill, not by the CLI. `specpin generate` prints a pointer to the skill and
|
|
73
|
+
does nothing else. Do not rely on it to produce specs.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Fingerprint strategy
|
|
2
|
+
|
|
3
|
+
You are not running inside the browser, so you cannot capture a live element.
|
|
4
|
+
You synthesize the fingerprint from source (JSX/HTML/Vue/Svelte) or a DOM dump.
|
|
5
|
+
Specpin is **non-intrusive by default**: it attaches to the UI you already have,
|
|
6
|
+
so do NOT edit the app's source unless the project opts in. Prefer an anchor the
|
|
7
|
+
element already carries; fall back to synthesized selectors; treat adding a new
|
|
8
|
+
attribute as an optional, opt-in upgrade.
|
|
9
|
+
|
|
10
|
+
## Decision tree
|
|
11
|
+
|
|
12
|
+
1. **Existing anchor (best, confidence 1.0, zero source changes).** If the
|
|
13
|
+
element already has a stable `data-testid` / `data-spec-id` / `data-cy` /
|
|
14
|
+
`data-qa`, set `fingerprint.testId` to it. A non-generated `id` or an
|
|
15
|
+
`aria-label` are also exact-ish anchors: set `fingerprint.id` / `ariaLabel`.
|
|
16
|
+
This is the default and preferred path: an exact match that survives refactors
|
|
17
|
+
without touching the app.
|
|
18
|
+
|
|
19
|
+
2. **Synthesize from the existing markup (no edits, lower confidence).** No ready
|
|
20
|
+
anchor: derive the required fields from the source as it is (see per-field
|
|
21
|
+
derivation below). Without an exact anchor a spec may render as `needsReview`
|
|
22
|
+
in the extension, but nothing in the app changes. This is the right default for
|
|
23
|
+
any project that does not want source modifications.
|
|
24
|
+
|
|
25
|
+
3. **Add a `data-spec-id` (optional, opt-in only).** ONLY when the project agrees
|
|
26
|
+
to small non-functional markup additions, add a `data-spec-id` for the most
|
|
27
|
+
resilient anchor, then fingerprint on it (step 1). Specpin works fine without
|
|
28
|
+
this; never edit source unless asked.
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// React
|
|
32
|
+
<button data-spec-id="nav-logout" type="button" onClick={logout}>Log out</button>
|
|
33
|
+
```
|
|
34
|
+
```html
|
|
35
|
+
<!-- HTML -->
|
|
36
|
+
<button data-spec-id="nav-logout" type="button">Log out</button>
|
|
37
|
+
```
|
|
38
|
+
```vue
|
|
39
|
+
<!-- Vue -->
|
|
40
|
+
<button data-spec-id="nav-logout" type="button" @click="logout">Log out</button>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then mirror it: `"testId": "nav-logout"`. Either way, the remaining fingerprint
|
|
44
|
+
fields are still required by the schema, so fill them as below.
|
|
45
|
+
|
|
46
|
+
### Per-field derivation (reading the source)
|
|
47
|
+
|
|
48
|
+
- `cssSelector`: shortest unique selector, e.g. `nav button[type=button]` or
|
|
49
|
+
`form.login input[type=email]`. Prefer classes/attributes over nth-child.
|
|
50
|
+
- `xpath`: e.g. `//nav//button[@type='button']`.
|
|
51
|
+
- `domPath`: ancestor-to-element tag chain, e.g. `["nav", "button"]`.
|
|
52
|
+
- `tagName`: lowercase tag, e.g. `"button"`.
|
|
53
|
+
- `attributes`: whitelisted only (role, type, name, placeholder, href),
|
|
54
|
+
string values, e.g. `{ "type": "button" }`.
|
|
55
|
+
- `positionHint`: `{ index, siblingCount }` among siblings (0-based index).
|
|
56
|
+
Estimate from source order; both are integers >= 0.
|
|
57
|
+
- `textContent` (optional, nullable): the visible text, normalized, e.g.
|
|
58
|
+
`"Log out"`; `null` for inputs.
|
|
59
|
+
- `ariaLabel`, `id` (optional, nullable): set if present; exclude
|
|
60
|
+
auto-generated ids like `":r1:"` or `"css-1a2b3c"` (use `null`).
|
|
61
|
+
- `nearbyLabels` (optional): visible labels near the element, e.g.
|
|
62
|
+
`["Email", "Password"]`.
|
|
63
|
+
- `frameworkHint` (optional): `"react" | "vue" | "angular" | "vanilla"`.
|
|
64
|
+
|
|
65
|
+
## Brittleness caveat
|
|
66
|
+
|
|
67
|
+
`positionHint` and `domPath` are the most fragile signals: any reorder or
|
|
68
|
+
wrapper change breaks them. The CSS selector is moderately robust. A test-id is
|
|
69
|
+
the only signal that survives a refactor unchanged. So for a critical element,
|
|
70
|
+
fingerprint on the most stable signal it already has (an existing test-id, a
|
|
71
|
+
non-generated `id`, or a unique selector); an added `data-spec-id` is the most
|
|
72
|
+
robust option only if the project allows source edits.
|
|
73
|
+
|
|
74
|
+
## Match order (current)
|
|
75
|
+
|
|
76
|
+
The extension matches in this order: exact anchors (test-id, confidence 1.0) ->
|
|
77
|
+
unique `cssSelector` (confidence 0.7) -> otherwise `needsReview`. The
|
|
78
|
+
`settings.matchConfidenceThreshold` in the manifest is reserved for the deferred
|
|
79
|
+
hybrid weighted scorer; exact-anchor and unique-cssSelector are the live paths
|
|
80
|
+
today.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Schema authoring (spec v1)
|
|
2
|
+
|
|
3
|
+
Distilled from the canonical schema `v1.json`
|
|
4
|
+
(`$id: https://specpin.ohnice.app/schema/v1.json`, JSON Schema draft 2020-12).
|
|
5
|
+
Every object is `additionalProperties: false`: unknown keys are rejected by both
|
|
6
|
+
the TS (ajv) and Go validators. Add `"$schema": "https://specpin.ohnice.app/schema/v1.json"`
|
|
7
|
+
to each file for editor autocomplete.
|
|
8
|
+
|
|
9
|
+
## Files in `.specs/`
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.specs/
|
|
13
|
+
manifest.json # index + project config (required)
|
|
14
|
+
<area>.spec.json # a group of specs (SpecFile)
|
|
15
|
+
views.json # optional team visibility defaults (do not author by hand)
|
|
16
|
+
guides.json # optional onboarding tours (do not author by hand)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Manifest (`manifest.json`)
|
|
20
|
+
|
|
21
|
+
| Field | Type | Required | Notes |
|
|
22
|
+
|-------|------|----------|-------|
|
|
23
|
+
| `version` | string | yes | e.g. `"1.0"` |
|
|
24
|
+
| `project` | string | yes | display name |
|
|
25
|
+
| `domains` | string[] | yes | origins the UI runs on, e.g. `["localhost:3000"]`; empty = any |
|
|
26
|
+
| `specFiles` | string[] | yes | names of the `<area>.spec.json` files |
|
|
27
|
+
| `settings.defaultLocale` | string | no | fallback locale |
|
|
28
|
+
| `settings.locales` | string[] | no | BCP-47 locales authored |
|
|
29
|
+
| `settings.matchConfidenceThreshold` | number 0-1 | no | reserved for the deferred hybrid scorer |
|
|
30
|
+
| `settings.defaultDisplayMode` | DisplayMode | no | fallback render mode |
|
|
31
|
+
|
|
32
|
+
## SpecFile (`<area>.spec.json`)
|
|
33
|
+
|
|
34
|
+
| Field | Type | Required |
|
|
35
|
+
|-------|------|----------|
|
|
36
|
+
| `group` | string (non-empty) | yes |
|
|
37
|
+
| `specs` | Spec[] | yes |
|
|
38
|
+
|
|
39
|
+
## Spec
|
|
40
|
+
|
|
41
|
+
| Field | Type | Required | Notes |
|
|
42
|
+
|-------|------|----------|-------|
|
|
43
|
+
| `id` | string (non-empty) | yes | unique within the project |
|
|
44
|
+
| `title` | LocalizedString | yes | locale-keyed object |
|
|
45
|
+
| `description` | LocalizedString | yes | locale-keyed object, each value non-empty |
|
|
46
|
+
| `businessRules` | LocalizedString[] | no | one locale-keyed object per rule |
|
|
47
|
+
| `tags` | string[] | no | NOT localized |
|
|
48
|
+
| `preferredDisplayMode` | DisplayMode | no | overrides `settings.defaultDisplayMode` |
|
|
49
|
+
| `fingerprint` | ElementFingerprint | yes | the element link |
|
|
50
|
+
| `meta` | SpecMeta | no | provenance + timestamps |
|
|
51
|
+
|
|
52
|
+
## LocalizedString
|
|
53
|
+
|
|
54
|
+
A locale-keyed object, NOT a flat string:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "en": "Log in button", "vi": "Nut dang nhap" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Keys are BCP-47 codes matching `^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$` (`en`, `vi`, `en-US`).
|
|
61
|
+
- At least one entry (`minProperties: 1`), at most 50; each value non-empty (`minLength: 1`).
|
|
62
|
+
- Flat strings are rejected: `"title": "Log in"` is invalid. Use `{ "en": "Log in" }`.
|
|
63
|
+
|
|
64
|
+
## ElementFingerprint
|
|
65
|
+
|
|
66
|
+
Required: `cssSelector`, `xpath`, `domPath`, `tagName`, `attributes`, `positionHint`.
|
|
67
|
+
Optional (all nullable where noted): `testId`, `ariaLabel`, `id`, `textContent`,
|
|
68
|
+
`nearbyLabels`, `frameworkHint`.
|
|
69
|
+
|
|
70
|
+
- `domPath`: tag chain array, e.g. `["form", "button"]`.
|
|
71
|
+
- `attributes`: object of whitelisted attrs (role, type, name, placeholder, href), string values.
|
|
72
|
+
- `positionHint`: `{ "index": int >= 0, "siblingCount": int >= 0 }` (both required).
|
|
73
|
+
- `frameworkHint`: `"react" | "vue" | "angular" | "vanilla"`.
|
|
74
|
+
|
|
75
|
+
See `fingerprint-strategy.md` for how to fill these from source.
|
|
76
|
+
|
|
77
|
+
## SpecMeta
|
|
78
|
+
|
|
79
|
+
All four required when `meta` is present: `createdBy` (string),
|
|
80
|
+
`createdAt` + `updatedAt` (RFC3339 date-time, format-checked by both validators),
|
|
81
|
+
`source` (`"ai-generated" | "manual"`). Use `"ai-generated"` for specs you author.
|
|
82
|
+
|
|
83
|
+
## DisplayMode
|
|
84
|
+
|
|
85
|
+
`"overlay" | "tooltip" | "sidebar" | "modal" | "inline-badge"`. Implemented:
|
|
86
|
+
`tooltip`, `sidebar`, `modal`. `overlay` and `inline-badge` are reserved and
|
|
87
|
+
fall back to `tooltip` at render time.
|
|
88
|
+
|
|
89
|
+
## Markdown subset (rendering convention only)
|
|
90
|
+
|
|
91
|
+
`description` and each `businessRules` item may carry a small Markdown subset.
|
|
92
|
+
The stored value stays a plain string (no schema change), so files remain
|
|
93
|
+
Git-diffable.
|
|
94
|
+
|
|
95
|
+
- Inline (both fields): bold `**text**`, italic `*text*` / `_text_`, links
|
|
96
|
+
`[label](url)`. Only `http`, `https`, `mailto` URLs render; others drop to text.
|
|
97
|
+
- Block (`description` only): bullet lists (`- ` / `* `), numbered lists (`1. `),
|
|
98
|
+
blank-line paragraphs, single newline = line break. A `businessRules` item is
|
|
99
|
+
inline-only (one line, one list item).
|
|
100
|
+
- Not supported (rendered literally): headings, blockquotes, code, tables,
|
|
101
|
+
images, underline. `title` is never interpreted as Markdown.
|
|
102
|
+
|
|
103
|
+
## Complete minimal valid example
|
|
104
|
+
|
|
105
|
+
`nav.spec.json`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"$schema": "https://specpin.ohnice.app/schema/v1.json",
|
|
110
|
+
"group": "Navigation",
|
|
111
|
+
"specs": [
|
|
112
|
+
{
|
|
113
|
+
"id": "nav-logout",
|
|
114
|
+
"title": { "en": "Log out button" },
|
|
115
|
+
"description": { "en": "Ends the session and returns to the login screen." },
|
|
116
|
+
"businessRules": [
|
|
117
|
+
{ "en": "Clears the auth token client-side before redirecting" }
|
|
118
|
+
],
|
|
119
|
+
"tags": ["auth"],
|
|
120
|
+
"preferredDisplayMode": "tooltip",
|
|
121
|
+
"fingerprint": {
|
|
122
|
+
"testId": "nav-logout",
|
|
123
|
+
"ariaLabel": null,
|
|
124
|
+
"id": null,
|
|
125
|
+
"cssSelector": "nav button[type=button]",
|
|
126
|
+
"xpath": "//nav//button[@type='button']",
|
|
127
|
+
"domPath": ["nav", "button"],
|
|
128
|
+
"tagName": "button",
|
|
129
|
+
"textContent": "Log out",
|
|
130
|
+
"attributes": { "type": "button" },
|
|
131
|
+
"nearbyLabels": ["Settings"],
|
|
132
|
+
"positionHint": { "index": 5, "siblingCount": 6 },
|
|
133
|
+
"frameworkHint": "react"
|
|
134
|
+
},
|
|
135
|
+
"meta": {
|
|
136
|
+
"createdBy": "agent",
|
|
137
|
+
"createdAt": "2026-06-30T00:00:00Z",
|
|
138
|
+
"updatedAt": "2026-06-30T00:00:00Z",
|
|
139
|
+
"source": "ai-generated"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Then add `"nav.spec.json"` to `manifest.json` `specFiles[]` and run
|
|
147
|
+
`specpin validate`.
|