@specpin/cli 0.0.1 → 0.0.2

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 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.1",
3
+ "version": "0.0.2",
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`.