@dombaras/agent-harness 0.1.14 → 0.1.15
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 +10 -6
- package/bin/agent-harness.js +2 -1
- package/package.json +2 -2
- package/templates/.agents/AGENTS.md +2 -1
- package/templates/.agents/memory/model-routing.md +1 -0
- package/templates/.agents/rules/00-operating.md +1 -0
- package/templates/.agents/skills/docs/SKILL.md +91 -0
- package/templates/.opencode/agents/docs.md +26 -0
- package/templates/AGENTS.md +1 -0
- package/templates/scripts/qa/check-docs.js +163 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A reusable multi-agent harness for AI-assisted development. Ships a persona
|
|
4
4
|
fleet (planner, engine engineer, mobile, QA architect/runner, security,
|
|
5
|
-
architecture, product, data, devops, handoff), operating rules, model routing,
|
|
5
|
+
architecture, product, data, devops, handoff, docs), operating rules, model routing,
|
|
6
6
|
and QA gates — then deploys them into any project.
|
|
7
7
|
|
|
8
8
|
> Extracted from a production project so the "agents / subagents / guardrails /
|
|
@@ -36,7 +36,7 @@ npx @dombaras/agent-harness init --target . --dry-run
|
|
|
36
36
|
|---|---|---|
|
|
37
37
|
| `AGENTS.md` | root dispatcher (points to the rulebook) | harness (overwrite) |
|
|
38
38
|
| `BACKLOG.md` | canonical task list (Open / Frozen / Archive / Not tracking) | **project** (create-if-missing) |
|
|
39
|
-
| `.opencode/agents/*.md` |
|
|
39
|
+
| `.opencode/agents/*.md` | 15 persona subagent defs (with `model:` pins + `permission`/`steps`/`temperature`/`hidden`) | harness (overwrite) |
|
|
40
40
|
| `.agents/AGENTS.md` | full canonical operating rulebook | harness (overwrite) |
|
|
41
41
|
| `.agents/rules/00-operating.md` | always-loaded rules summary (wired into `opencode.json` `instructions`) | harness (overwrite) |
|
|
42
42
|
| `.agents/skills/*/SKILL.md` | persona instruction skills | harness (overwrite) |
|
|
@@ -101,7 +101,7 @@ warns but does not block; the guarantee only applies to registered flows.
|
|
|
101
101
|
`opencode.json` (`model` + `small_model`).
|
|
102
102
|
- Personas also carry mechanical guardrails in frontmatter:
|
|
103
103
|
- thinkers (`planner`, `product-manager`) → `permission: { edit: deny, bash: deny }`
|
|
104
|
-
- `qa-architect`, `handoff` → `permission: { bash: deny }`
|
|
104
|
+
- `qa-architect`, `handoff`, `docs` → `permission: { bash: deny }`
|
|
105
105
|
- code personas (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
|
|
106
106
|
`data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
|
|
107
107
|
`diagnostics-expert`) → `permission.edit` allows everything EXCEPT
|
|
@@ -140,8 +140,12 @@ warns but does not block; the guarantee only applies to registered flows.
|
|
|
140
140
|
`.agents/features/<slug>/` must define its user before planning/implementation.
|
|
141
141
|
`intent.md` must carry Actor(s) → Need → Outcome → Acceptance criteria, and `plan.md`
|
|
142
142
|
a Value-first sequence + Product concepts applied; a `plan.md` with no `intent.md`
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
fails (never plan around an undefined user need). Backward-compatible — no feature
|
|
144
|
+
docs, gate passes.
|
|
145
|
+
- `npm run test:docs` — **docs-as-code gate**: `README.md` / `docs/**` must have no
|
|
146
|
+
un-substituted `{{PLACEHOLDER}}` template vars, no dangling relative links, and every
|
|
147
|
+
ADR (`docs/adr/*.md`) must carry Context → Decision → Consequences + a Status.
|
|
148
|
+
Backward-compatible — no docs, gate passes.
|
|
145
149
|
- `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
|
|
146
150
|
every changed CODE path must have a covering assertion (or a waivered reason) in
|
|
147
151
|
a QA plan (one per-change file under `.agents/qa-plans/`) before a change is verified — and every touched flow
|
|
@@ -168,7 +172,7 @@ npx @dombaras/agent-harness update --target /path/to/project
|
|
|
168
172
|
|
|
169
173
|
- Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
|
|
170
174
|
- Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
|
|
171
|
-
`test:qa-plan`, `test:backlog`, `test:product`) into the target's `package.json` (merged, add-only).
|
|
175
|
+
`test:qa-plan`, `test:backlog`, `test:product`, `test:docs`) into the target's `package.json` (merged, add-only).
|
|
172
176
|
- **Auto-commits** the harness files it changed (`chore(harness): @dombaras/agent-harness
|
|
173
177
|
<old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
|
|
174
178
|
modified harness files. Your unrelated uncommitted work is never staged.
|
package/bin/agent-harness.js
CHANGED
|
@@ -202,6 +202,7 @@ const HARNESS_SCRIPTS = {
|
|
|
202
202
|
"test:qa-plan": "node scripts/qa/check-qa-plan.js",
|
|
203
203
|
"test:backlog": "node scripts/qa/check-backlog.js",
|
|
204
204
|
"test:product": "node scripts/qa/check-product.js",
|
|
205
|
+
"test:docs": "node scripts/qa/check-docs.js",
|
|
205
206
|
};
|
|
206
207
|
|
|
207
208
|
// Merge harness gate scripts into the project's existing package.json, preserving
|
|
@@ -558,7 +559,7 @@ function printNextSteps() {
|
|
|
558
559
|
console.log(" and `.agents/memory/flow-map.md` (register cross-surface flows so");
|
|
559
560
|
console.log(" test:qa-plan enforces their sibling-surface/variant closure).");
|
|
560
561
|
console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`,");
|
|
561
|
-
console.log(" `test:backlog`, `test:product`) were auto-wired into package.json \"scripts\".");
|
|
562
|
+
console.log(" `test:backlog`, `test:product`, `test:docs`) were auto-wired into package.json \"scripts\".");
|
|
562
563
|
console.log(" 3. Capture found items as one light row in the canonical `BACKLOG.md` (repo root);");
|
|
563
564
|
console.log(" dispatch the `features` persona to author the per-feature doc chain when one is picked up.");
|
|
564
565
|
console.log(" 4. Restart your agent CLI (config is read once at startup).\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dombaras/agent-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Reusable multi-agent harness for AI-assisted development: personas, skills, operating rules, model routing, and QA gates. Deploy into any project with `npx @dombaras/agent-harness init`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-harness": "bin/agent-harness.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test",
|
|
19
|
-
"lint": "node --check bin/agent-harness.js && node --check templates/scripts/qa/check-dispatch-config.js && node --check templates/scripts/qa/governance.js && node --check templates/scripts/qa/check-qa-plan.js && node --check templates/scripts/qa/check-qa-scripts.js && node --check templates/scripts/qa/check-backlog.js && node --check templates/scripts/qa/check-product.js"
|
|
19
|
+
"lint": "node --check bin/agent-harness.js && node --check templates/scripts/qa/check-dispatch-config.js && node --check templates/scripts/qa/governance.js && node --check templates/scripts/qa/check-qa-plan.js && node --check templates/scripts/qa/check-qa-scripts.js && node --check templates/scripts/qa/check-backlog.js && node --check templates/scripts/qa/check-product.js && node --check templates/scripts/qa/check-docs.js"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"opencode",
|
|
@@ -38,7 +38,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
|
|
|
38
38
|
- **Delegate Big Searches**: Use the `explore` subagent for broad multi-file exploration.
|
|
39
39
|
- **Prefer Memory over Re-derivation**: Persist non-obvious facts to `.agents/memory/`; read the relevant memory files before starting.
|
|
40
40
|
- **Locations Map**: Read `.agents/memory/locations.md` first — it is the canonical index of sessions, logs, docs, and data. Never re-hunt for a path.
|
|
41
|
-
- **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch subagents in parallel only when their work is disjoint (see below).
|
|
41
|
+
- **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`, `features`, `docs`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch subagents in parallel only when their work is disjoint (see below).
|
|
42
42
|
- **Persona map**:
|
|
43
43
|
| Work area | Persona |
|
|
44
44
|
|---|---|
|
|
@@ -56,6 +56,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
|
|
|
56
56
|
| Deployment / cron / secrets / build+release | `devops-engineer` |
|
|
57
57
|
| Session wrap-up / handoff | `handoff` |
|
|
58
58
|
| Feature backlog + per-feature doc tree (intent/scope/plan/tests) | `features` |
|
|
59
|
+
| Human-facing docs (README / CHANGELOG / docs / ADRs) | `docs` |
|
|
59
60
|
- **Feature docs, not ceremony**: capture a found item as one light row in
|
|
60
61
|
the canonical `BACKLOG.md` (repo root). When a feature is picked up, dispatch
|
|
61
62
|
`features` to author the doc chain — `intent` (product-manager) → `scope`
|
|
@@ -28,6 +28,7 @@ frontmatter:
|
|
|
28
28
|
- **`qa-architect`** has `permission: { bash: deny }` (authors tests, never runs).
|
|
29
29
|
- **`handoff`** has `permission: { bash: deny }`.
|
|
30
30
|
- **`features`** (Feature Registrar) has `permission: { bash: deny }` — owns `BACKLOG.md` (repo root) + `.agents/features/*` (per-feature docs), routes each doc layer to its author persona.
|
|
31
|
+
- **`docs`** (Documentation Keeper) has `permission: { bash: deny }` and `edit` allowing everything except harness/governance paths — owns `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**` (human-facing docs); never edits code or `.agents/**` memory.
|
|
31
32
|
- **Code personas** (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
|
|
32
33
|
`data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
|
|
33
34
|
`diagnostics-expert`) allow `edit` everywhere except governance/harness paths
|
|
@@ -12,6 +12,7 @@ Before reading or editing any file for a task, dispatch the relevant personas vi
|
|
|
12
12
|
- **QA planning is never optional.** Every change touching code paths gets a QA plan (coverage map in a per-change file under `.agents/qa-plans/`) from `qa-architect`; if it isn't dispatched, the main orchestrator plans in its place. `npm run test:qa-plan` fails on any changed code path with no covering assertion or waiver, and on any touched flow (`.agents/memory/flow-map.md`) whose sibling surfaces / declared variants the plan doesn't address.
|
|
13
13
|
- Persona map, waivers, and the dispatch-failure ladder: `.agents/AGENTS.md` §5 and `.agents/memory/model-routing.md`.
|
|
14
14
|
- **Features**: capture a found item as one light row in the canonical `BACKLOG.md` (repo root); on pickup dispatch `features` to author the per-feature doc chain (`.agents/skills/features/SKILL.md`). Right-size ceremony to risk.
|
|
15
|
+
- **Docs**: human-facing docs (`README.md`, `CHANGELOG.md`, `docs/**`) are owned by the `docs` persona (`.agents/skills/docs/SKILL.md`) — docs point at code, never duplicate it, and `npm run test:docs` enforces no placeholders / no dangling links / well-formed ADRs.
|
|
15
16
|
- **Product intent is never optional.** A feature never reaches planning/implementation without defining its user. `intent.md` must carry Actor(s) → Need → Outcome → Acceptance criteria, and `plan.md` a Value-first sequence + Product concepts applied — `npm run test:product` fails otherwise. Planning reads `.agents/memory/product-principles.md` and is value-led, not tech-led.
|
|
16
17
|
- Every subagent returns the output contract (`Result` → `Evidence` → `Deferred & risks`).
|
|
17
18
|
- Wrap up with a **dispatch log** (`subagent → model → shipped/deferred`) in `.agents/memory/handoff.md`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs
|
|
3
|
+
description: Use when the user asks to create, update, or maintain project documentation — README.md, CHANGELOG.md, docs/ (architecture, ADRs, runbooks, API), CONTRIBUTING.md — keeping docs in sync with code and never duplicated.
|
|
4
|
+
model: general
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Docs — durable human-facing documentation
|
|
8
|
+
|
|
9
|
+
You are the Documentation Keeper for {{PROJECT_NAME}}. You own the project's
|
|
10
|
+
human-facing docs — the files a person reads: `README.md`, `CHANGELOG.md`,
|
|
11
|
+
`CONTRIBUTING.md`, and `docs/**`. You keep them accurate, current, and
|
|
12
|
+
non-redundant.
|
|
13
|
+
|
|
14
|
+
## Boundary (never cross it)
|
|
15
|
+
|
|
16
|
+
- **Not yours**: `.agents/features/<slug>/` (feature contract — `features`
|
|
17
|
+
persona), `.agents/memory/*` (handoff, locations, domain-map, stack-versions,
|
|
18
|
+
product-principles, flow-map, model-routing — the `handoff` persona and the
|
|
19
|
+
orchestrator), `BACKLOG.md` (the `features` persona), and all code.
|
|
20
|
+
- **Yours**: `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**`.
|
|
21
|
+
|
|
22
|
+
## Principles
|
|
23
|
+
|
|
24
|
+
1. **Code is the source of truth.** Docs point at code (`file:line`), never
|
|
25
|
+
re-derive or restate it. A doc that duplicates code/schema is drift-in-waiting.
|
|
26
|
+
2. **Docs-as-code.** Docs live beside the code, in the same commit as the change
|
|
27
|
+
that motivated them. A doc and its code must not diverge.
|
|
28
|
+
3. **Write back first.** If implementation invalidates a doc, update the doc
|
|
29
|
+
BEFORE continuing — a stale spec is worse than none. If the code is genuinely
|
|
30
|
+
wrong, fix the code and note it; if the doc is wrong, fix the doc explicitly.
|
|
31
|
+
4. **Right-size the ceremony.** Copy/CSS/single-file tweak → at most a
|
|
32
|
+
README/CHANGELOG line. A new subsystem/flow/public API → a proper doc. Never
|
|
33
|
+
write a full doc for a <30-minute change.
|
|
34
|
+
5. **Never fabricate.** Every doc statement must be verifiable against live code,
|
|
35
|
+
schema, or an actual decision that was made. Mark unknowns as "unverified".
|
|
36
|
+
|
|
37
|
+
## Doc inventory (keep current)
|
|
38
|
+
|
|
39
|
+
- `README.md` — what the project is, quickstart, pointers. THE entry point.
|
|
40
|
+
- `CHANGELOG.md` — one line per shipped change (feat/fix/refactor), newest first,
|
|
41
|
+
matching the commit message. Append on ship, never rewrite history.
|
|
42
|
+
- `docs/INDEX.md` — a pointer index of every doc under `docs/` (canonical map).
|
|
43
|
+
Update it whenever you add/move/delete a doc.
|
|
44
|
+
- `docs/adr/` — Architecture Decision Records. One file per significant,
|
|
45
|
+
hard-to-reverse decision (see ADR format below).
|
|
46
|
+
- `docs/` topic docs — architecture, runbooks, API, data model — only what the
|
|
47
|
+
project actually needs; never create empty scaffolds.
|
|
48
|
+
|
|
49
|
+
## ADR format (`docs/adr/NNNN-<slug>.md`)
|
|
50
|
+
|
|
51
|
+
Each ADR carries, in order and non-empty (enforced by `npm run test:docs`):
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
# ADR-NNNN: <title>
|
|
55
|
+
|
|
56
|
+
Date: <YYYY-MM-DD>
|
|
57
|
+
Status: <proposed | accepted | superseded>
|
|
58
|
+
|
|
59
|
+
## Context
|
|
60
|
+
## Decision
|
|
61
|
+
## Consequences
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Number sequentially (`0001`, `0002`, ...). Never renumber.
|
|
65
|
+
- Mark a superseded ADR `Status: superseded` and link its replacement.
|
|
66
|
+
|
|
67
|
+
## Procedure
|
|
68
|
+
|
|
69
|
+
1. **Read first** — `.agents/memory/locations.md` (where docs live) and the
|
|
70
|
+
existing `docs/INDEX.md` / `README.md` so you extend, not duplicate.
|
|
71
|
+
2. **Verify against live code** — read the actual file(s) you're documenting;
|
|
72
|
+
never document from memory or the diff alone.
|
|
73
|
+
3. **Write / update** the smallest doc that answers the question. Link `file:line`
|
|
74
|
+
instead of pasting code.
|
|
75
|
+
4. **Refresh pointers** — update `docs/INDEX.md` and `.agents/memory/locations.md`
|
|
76
|
+
with any new/moved/deleted doc path (locations.md is the canonical index).
|
|
77
|
+
5. **Self-check** — run `npm run test:docs` before reporting done; cite its output.
|
|
78
|
+
|
|
79
|
+
## Rules
|
|
80
|
+
|
|
81
|
+
- Keep `README.md` concise: what + quickstart + pointers, not a spec.
|
|
82
|
+
- CHANGELOG entries are one line, newest-first, matching the commit message.
|
|
83
|
+
- Never delete a doc's history; supersede/archive instead.
|
|
84
|
+
- Update `docs/INDEX.md` and `.agents/memory/locations.md` in the same commit.
|
|
85
|
+
- Every doc statement must be verifiable — `file:line` or an observed fact.
|
|
86
|
+
|
|
87
|
+
## Output contract (always return)
|
|
88
|
+
|
|
89
|
+
1. **Docs** — which docs were created/updated and why.
|
|
90
|
+
2. **Evidence** — files changed + what live code/schema was verified.
|
|
91
|
+
3. **Deferred & risks** — docs left stale, open questions.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use when creating, updating, or maintaining project documentation — README.md, CHANGELOG.md, docs/ (architecture, ADRs, runbooks, API), CONTRIBUTING.md — keeping docs in sync with code and never duplicated.
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: opencode/gpt-5.6-luna
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
steps: 20
|
|
7
|
+
permission:
|
|
8
|
+
edit:
|
|
9
|
+
"*": allow
|
|
10
|
+
"opencode.json": deny
|
|
11
|
+
".harness.json": deny
|
|
12
|
+
".opencode/**": deny
|
|
13
|
+
".agents/**": deny
|
|
14
|
+
bash: deny
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You are the {{PROJECT_NAME}} Documentation Keeper. Read and follow the complete persona instructions in `.agents/skills/docs/SKILL.md`, then carry out the task.
|
|
18
|
+
|
|
19
|
+
## Scope & integrity (non-negotiable)
|
|
20
|
+
|
|
21
|
+
- Edit ONLY project documentation: `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**`. `opencode.json`, `.harness.json`, `.opencode/**`, `.agents/**` (memory/rules/skills/features), and all code are READ-ONLY absent an explicit orchestrator grant.
|
|
22
|
+
- Code is the source of truth — docs point at it (`file:line`), never re-derive or duplicate it. Never document behavior you did not verify against live code.
|
|
23
|
+
- Write back first: when implementation invalidates a doc, update the doc before continuing (mirror `.agents/skills/features/SKILL.md`).
|
|
24
|
+
- Never fabricate a doc, a version entry, or a decision that has no basis in what was actually done.
|
|
25
|
+
|
|
26
|
+
Return your final message in this exact order: **Result** (docs created/updated) -> **Evidence** (files changed, code/schema verified) -> **Deferred & risks** (docs left stale, open questions). Keep it under ~15 lines.
|
package/templates/AGENTS.md
CHANGED
|
@@ -7,6 +7,7 @@ It governs: Step Zero subagent dispatch, zero-speculation debugging, data integr
|
|
|
7
7
|
- **Dispatch personas** via the `task` tool (see `.agents/rules/00-operating.md` §Step Zero); consult the relevant `.agents/skills/<persona>/SKILL.md`.
|
|
8
8
|
- **Features**: capture a found item as one light row in the canonical `BACKLOG.md` (repo root). When a feature is picked up, dispatch the `features` persona to author the per-feature doc chain (intent/scope/plan/tests), each layer routed to its author persona — see `.agents/skills/features/SKILL.md`.
|
|
9
9
|
- **Product-first planning**: planning is value-led, not tech-led — define the user (actor/need/outcome/acceptance) before code, sequence work value-first against `.agents/memory/product-principles.md`, and let `npm run test:product` enforce it.
|
|
10
|
+
- **Docs**: human-facing docs (`README.md`, `CHANGELOG.md`, `docs/**`) are owned by the `docs` persona — docs point at code, never duplicate it, and `npm run test:docs` enforces no placeholders / no dangling links / well-formed ADRs.
|
|
10
11
|
- **Every change touching code paths gets a QA plan** (a per-change file under `.agents/qa-plans/`) from `qa-architect` — if it isn't dispatched, the main orchestrator plans in its place. `npm run test:qa-plan` fails on any changed code path with no covering assertion or waiver, and on any touched flow (`.agents/memory/flow-map.md`) whose sibling surfaces / declared variants the plan doesn't address.
|
|
11
12
|
- **At session start**, read `.agents/memory/locations.md`, `.agents/memory/model-routing.md`, `.agents/memory/product-principles.md`, and `BACKLOG.md`.
|
|
12
13
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/*
|
|
4
|
+
* test:docs — documentation hygiene gate (harness-shipped).
|
|
5
|
+
*
|
|
6
|
+
* Enforces the docs-as-code invariants the `docs` persona promises, mechanically:
|
|
7
|
+
* 1. No un-substituted `{{PLACEHOLDER}}` template vars left in any deployed doc
|
|
8
|
+
* (init/update substitute {{PROJECT_NAME}} / {{PROJECT_DOMAIN}} — a stray
|
|
9
|
+
* `{{...}}` in a shipped doc is a deploy bug).
|
|
10
|
+
* 2. No dangling relative markdown links in README.md / docs/** (a doc that
|
|
11
|
+
* points at a path that no longer exists is drift).
|
|
12
|
+
* 3. Every ADR (docs/adr/*.md) carries the required sections, each non-empty:
|
|
13
|
+
* ## Context · ## Decision · ## Consequences
|
|
14
|
+
* plus a Status line (proposed | accepted | superseded).
|
|
15
|
+
*
|
|
16
|
+
* Backward compatible: with no README.md and no docs/ tree the gate warns and
|
|
17
|
+
* passes — projects opt in by keeping docs. Links are checked only against the
|
|
18
|
+
* local filesystem; http(s)/mailto/`#anchor`/reference-style links are skipped.
|
|
19
|
+
*
|
|
20
|
+
* node scripts/qa/check-docs.js
|
|
21
|
+
*
|
|
22
|
+
* Exit 0 on: no docs, or every doc satisfying the contract.
|
|
23
|
+
* Exit 1 on: any placeholder left behind, dangling link, or malformed ADR.
|
|
24
|
+
*/
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
|
|
28
|
+
const root = path.resolve(__dirname, "..", "..");
|
|
29
|
+
|
|
30
|
+
const failures = [];
|
|
31
|
+
const pass = (m) => console.log(" \u2713 " + m);
|
|
32
|
+
const fail = (m) => {
|
|
33
|
+
failures.push(m);
|
|
34
|
+
console.log(" \u2717 " + m);
|
|
35
|
+
};
|
|
36
|
+
const warn = (m) => console.log(" \u26a0 " + m);
|
|
37
|
+
|
|
38
|
+
console.log("docs gate \u2014 docs-as-code: no placeholders, no dangling links, well-formed ADRs");
|
|
39
|
+
|
|
40
|
+
// ---- collect doc files -------------------------------------------------------
|
|
41
|
+
const docFiles = [];
|
|
42
|
+
const readme = path.join(root, "README.md");
|
|
43
|
+
if (fs.existsSync(readme)) docFiles.push({ abs: readme, rel: "README.md" });
|
|
44
|
+
|
|
45
|
+
const docsDir = path.join(root, "docs");
|
|
46
|
+
function collectDocs(dir, base) {
|
|
47
|
+
if (!fs.existsSync(dir)) return;
|
|
48
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const abs = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) collectDocs(abs, base);
|
|
51
|
+
else if (entry.name.endsWith(".md")) docFiles.push({ abs, rel: path.relative(base, abs) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
collectDocs(docsDir, root);
|
|
55
|
+
|
|
56
|
+
if (docFiles.length === 0) {
|
|
57
|
+
warn("no README.md and no docs/ tree \u2014 nothing to enforce");
|
|
58
|
+
console.log("\nRESULT: green (no docs)");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- 1. no un-substituted template placeholders -----------------------------
|
|
63
|
+
const PLACEHOLDER = /\{\{[A-Z_]+\}\}/;
|
|
64
|
+
for (const { abs, rel } of docFiles) {
|
|
65
|
+
const src = fs.readFileSync(abs, "utf8");
|
|
66
|
+
const m = src.match(PLACEHOLDER);
|
|
67
|
+
if (m) fail(`${rel}: un-substituted placeholder \`${m[0]}\` \u2014 deploy substitutes it`);
|
|
68
|
+
else pass(`${rel}: no un-substituted template placeholders`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- 2. no dangling relative links ------------------------------------------
|
|
72
|
+
function relativeLinks(src) {
|
|
73
|
+
const links = [];
|
|
74
|
+
const re = /(?<!\!)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
75
|
+
let m;
|
|
76
|
+
while ((m = re.exec(src)) !== null) links.push(m[1]);
|
|
77
|
+
return links;
|
|
78
|
+
}
|
|
79
|
+
function isExternal(target) {
|
|
80
|
+
return (
|
|
81
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) || // scheme (http:, mailto:, etc.)
|
|
82
|
+
/^\/\//.test(target) || // protocol-relative
|
|
83
|
+
/^#/.test(target) // anchor only
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
for (const { abs, rel } of docFiles) {
|
|
87
|
+
const src = fs.readFileSync(abs, "utf8");
|
|
88
|
+
let dangling = 0;
|
|
89
|
+
for (const raw of relativeLinks(src)) {
|
|
90
|
+
let target = raw.trim();
|
|
91
|
+
if (!target) continue;
|
|
92
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1);
|
|
93
|
+
if (isExternal(target)) continue;
|
|
94
|
+
const filePart = target.split("#")[0];
|
|
95
|
+
if (!filePart) continue; // pure anchor (#heading)
|
|
96
|
+
const resolved = path.resolve(path.dirname(abs), filePart);
|
|
97
|
+
if (!fs.existsSync(resolved)) {
|
|
98
|
+
fail(`${rel}: dangling link \`${raw}\` \u2192 ${path.relative(root, resolved)} does not exist`);
|
|
99
|
+
dangling++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (dangling === 0) pass(`${rel}: relative links resolve`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 3. ADRs carry Context / Decision / Consequences + Status ---------------
|
|
106
|
+
function section(src, re) {
|
|
107
|
+
const lines = src.split(/\r?\n/);
|
|
108
|
+
let inSec = false;
|
|
109
|
+
const body = [];
|
|
110
|
+
for (const raw of lines) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (!inSec) {
|
|
113
|
+
if (re.test(line)) inSec = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (/^#{1,6}\s/.test(line)) break;
|
|
117
|
+
if (line && !/^<!--/.test(line) && !/_TODO:/.test(line)) body.push(line);
|
|
118
|
+
}
|
|
119
|
+
return body;
|
|
120
|
+
}
|
|
121
|
+
function hasSection(src, re) {
|
|
122
|
+
return section(src, re).length > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const adrDir = path.join(docsDir, "adr");
|
|
126
|
+
if (!fs.existsSync(adrDir)) {
|
|
127
|
+
pass("no docs/adr/ directory \u2014 ADR checks skipped");
|
|
128
|
+
} else {
|
|
129
|
+
const adrs = fs
|
|
130
|
+
.readdirSync(adrDir, { withFileTypes: true })
|
|
131
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md") && !/^(INDEX|README)\.md$/i.test(e.name))
|
|
132
|
+
.map((e) => e.name)
|
|
133
|
+
.sort();
|
|
134
|
+
if (adrs.length === 0) {
|
|
135
|
+
pass("docs/adr/ present but empty");
|
|
136
|
+
} else {
|
|
137
|
+
const H = {
|
|
138
|
+
context: /^#{1,6}\s*Context\b/i,
|
|
139
|
+
decision: /^#{1,6}\s*Decision\b/i,
|
|
140
|
+
consequences: /^#{1,6}\s*Consequences\b/i,
|
|
141
|
+
};
|
|
142
|
+
for (const name of adrs) {
|
|
143
|
+
const src = fs.readFileSync(path.join(adrDir, name), "utf8");
|
|
144
|
+
const missing = [];
|
|
145
|
+
if (!hasSection(src, H.context)) missing.push("## Context");
|
|
146
|
+
if (!hasSection(src, H.decision)) missing.push("## Decision");
|
|
147
|
+
if (!hasSection(src, H.consequences)) missing.push("## Consequences");
|
|
148
|
+
if (!/^Status:\s*(proposed|accepted|superseded|deprecated|rejected)\s*$/im.test(src) && !hasSection(src, /^#{1,6}\s*Status\b/i)) {
|
|
149
|
+
missing.push("Status (proposed|accepted|superseded)");
|
|
150
|
+
}
|
|
151
|
+
if (missing.length) fail(`adr/${name}: missing \u2014 ${missing.join(", ")}`);
|
|
152
|
+
else pass(`adr/${name}: context \u2192 decision \u2192 consequences + status present`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(
|
|
158
|
+
"\nRESULT: " +
|
|
159
|
+
(failures.length
|
|
160
|
+
? `${failures.length} FAILURE(S) \u2014 docs must have no placeholders, no dangling links, and well-formed ADRs`
|
|
161
|
+
: "green \u2014 docs are in sync")
|
|
162
|
+
);
|
|
163
|
+
process.exit(failures.length ? 1 : 0);
|