Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ jobs:
# Individual check steps below stay non-blocking (continue-on-error: true): they
# print each tool's raw findings for review. Enforcement is done by the
# no-new-violations comparator, which diffs a fresh run against baseline.json.
# The gated rules are A1/A6/A7/B2/D2/ANCHOR + MrDocs-no-warnings + C2/C4/C9/C10.
# The gated rules are A1/A6/A7/B2/B6/D2/ANCHOR + MrDocs-no-warnings + C2/C4/C9/C10.
# The gate step below names them via --gate and REPORTS them: it prints each with
# file, line and an excerpt, and annotates it on the diff, but runs without
# --strict, so it does not fail the job. It keeps continue-on-error: false so a
Expand Down Expand Up @@ -358,7 +358,7 @@ jobs:
# job (no --strict). The two when_any.hpp C2 refusals this once expected are
# gone — resolved before #383 — and the live gated findings are #383's own.
node lint/check-no-new-violations.mjs \
--gate 'doc_lint:^(A1|A6|B2|D2|ANCHOR):' \
--gate 'doc_lint:^(A1|A6|B2|B6|D2|ANCHOR):' \
--gate 'vale_adoc:Capy\.PartHeadings$' \
--gate 'mrdocs_warnings:.*' \
--gate 'sentence_length:^C2:' \
Expand Down
11 changes: 10 additions & 1 deletion doc/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ The Antora pipeline provides two mechanisms; use them instead of hand-authoring:
suppressions live in `test/doc/doc_warnings.hpp`, included once per file outside every tag; a
warning that fires in one fragment stays in that fragment with a comment saying why. B5 is
enforced by review only — no script checks it.
- **B6. Every `@see` target in a docstring is an `@ref`.** Write `@see @ref task, @ref Executor`,
never `@see task, Executor`. MrDocs treats the text after `@see` as an ordinary paragraph and
does not auto-link names in it (Doxygen does), so a bare name renders as plain text in the
"See Also" section. Nothing warns about it either: `warn-broken-ref` checks only `@ref`
targets. Keep the line to the list of links. Prose after `@see` ("which describes teardown")
renders unlinked too, so move it into the description. When a name inside a class resolves
to the wrong symbol (`@ref execution_context` inside `execution_context::service` finds the
constructor), qualify it. `doc-lint.mjs` checks every `@see` paragraph in the public headers,
including the lines it wraps onto.

## Part C — Wording (pragmatic Simplified Technical English)

Expand Down Expand Up @@ -236,7 +245,7 @@ Not every rule is machine-checkable. Each rule sits in one of three tiers:

| Tier | Rules |
|---|---|
| **Gate** | A1, A6, A7, B2, B3, C2, C4, C9, C10, D2 |
| **Gate** | A1, A6, A7, B2, B3, B6, C2, C4, C9, C10, D2 |
| **Warning** | A2, B1, C1, C3, C5, C6, D4, D5, E1 |
| **Review** | A3, A4, A5, B4, B5, C7, C8, C11, D1, D3, E2, E3, E4 |

Expand Down
6 changes: 3 additions & 3 deletions doc/lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ site build. Node built-ins only, no dependencies of their own.

| Script | What it checks |
|---|---|
| `doc-lint.mjs` | Structural AsciiDoc/nav rules (A1, A6, B2, ANCHOR, D2, …). JSON on stdout. |
| `doc-lint.mjs` | Structural AsciiDoc/nav rules (A1, A6, B2, ANCHOR, D2, …), plus B6 (`@see` without `@ref`) over the public headers. JSON on stdout. |
| `extract-docstrings.mjs` | Extracts header docstrings into `.docstrings/*.adoc` so Vale can lint them. |
| `sentence-length.mjs` | **The authority for C2** (no sentence over 25 words), over both corpora. JSON on stdout. |
| ↳ | Emits `C2` (hard slice), `advisory-C2` (design essays) and `BACKTICK` (unbalanced code span). |
Expand Down Expand Up @@ -113,7 +113,7 @@ the rule at the head. Verified by planting a violation and confirming the finger
inside a delimited block `[[` is literal and renders fine, and that negative was bite-tested too.

`doc-lint.mjs` also emits a `SHAPE` rule alongside `A1`/`A6`/`B2`/`ANCHOR`/`D2` — deliberately outside the
gate spec's `^(A1|A6|B2|D2|ANCHOR):` alternation, the same way `advisory-C2` sits outside
gate spec's `^(A1|A6|B2|B6|D2|ANCHOR):` alternation, the same way `advisory-C2` sits outside
`sentence_length`'s `^C2:`. SHAPE is a content-shape heuristic over every `role=output`/
`role=figure` bare listing (doc/STYLE_GUIDE.md B3): those roles are a permanent B2 exemption, so a
block wrongly tagged non-code would otherwise be permanently invisible. A SHAPE finding never
Expand All @@ -124,7 +124,7 @@ fingerprint, and for the Vale checks the check name is at the **tail**, so `^Cap
matches nothing and the comparator then reports `gated: true, gatedNew: 0` — a gate that
announces it is gating while checking nothing, at exit 0. Measured twice on this branch. The
head-anchored shape is correct only for the two rule-at-the-head checks (`doc_lint`,
`sentence_length`), which is why the live spec mixes `^(A1|A6|B2|D2|ANCHOR):` and `^C2:` with
`sentence_length`), which is why the live spec mixes `^(A1|A6|B2|B6|D2|ANCHOR):` and `^C2:` with
`Capy\.PartHeadings$` and `(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$`. When you add
a gate, plant a violation of that exact rule and watch the step fail before you believe it.

Expand Down
8 changes: 6 additions & 2 deletions doc/lint/baseline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,13 @@ function docLintFingerprints() {
for (const it of items) {
const fp = occurrenceKey(seen, `${check}:${it.file}`, it.message);
fingerprints.push(fp);
// doc-lint reports paths relative to the pages tree, not to doc/.
// doc-lint reports paths relative to the pages tree, not to doc/ --
// except B6, which scans headers and reports repo-relative paths.
if (wantDetails) {
details[fp] = { ...resolveSource(`modules/ROOT/pages/${it.file}`, it.line ?? null, null),
const src = it.file.startsWith('include/')
? { file: it.file, line: it.line ?? null }
: resolveSource(`modules/ROOT/pages/${it.file}`, it.line ?? null, null);
details[fp] = { ...src,
rule: check, message: it.message, excerpt: null };
}
}
Expand Down
70 changes: 69 additions & 1 deletion doc/lint/doc-lint.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@
// block-depth skip rather than a whole-file regex. Bite-test per
// style-guide F4: put `` `[[nodiscard]]` `` in a page and confirm
// ANCHOR fires.
// B6 — every `@see` line in a public header names its targets with
// `@ref`. MrDocs does not auto-link names in `@see` (Doxygen does),
// so `@see task` renders as plain text in "See Also" and nothing
// warns: `warn-broken-ref` only checks `@ref` targets. The line must
// be `@see @ref a, @ref b` and nothing else; prose after `@see` also
// renders unlinked, so it fires too. The lines a `@see` paragraph
// wraps onto are held to the same rule. Scans include/boost/capy minus
// detail/ (the same scope as extract-docstrings.mjs). This is the one
// rule here that reads headers, not pages, so its `file` is
// repo-relative (`include/...`). Bite-test per style-guide F4: write
// `@see task` in a header and confirm B6 fires.
// D2 — every page in a CONCEPT_DIRS chapter (or quick-start.adoc) has
// >=1 include::example$. D2's "concept page" is a pedagogical
// category, not a Diátaxis mode — deliberately independent of
Expand All @@ -81,6 +92,8 @@ const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..')
// pages-tree override is for (exercising B2 in isolation).
const PAGES_DIR = process.argv[2] ? path.resolve(process.argv[2]) : path.join(ROOT, 'modules/ROOT/pages');
const NAV_FILE = path.join(ROOT, 'modules/ROOT/nav.adoc');
// B6's header tree; a second CLI arg overrides it for the self-test.
const HEADERS_DIR = process.argv[3] ? path.resolve(process.argv[3]) : path.join(ROOT, '../include/boost/capy');

// The four Diátaxis modes doc/STYLE_GUIDE.md Part A defines. A1 rejects
// anything else, including the legacy `concept` value (never a Diátaxis
Expand Down Expand Up @@ -270,7 +283,55 @@ function scanAnchors(lines) {
return out;
}

const findings = { A1: [], A6: [], B2: [], SHAPE: [], ANCHOR: [], D2: [] };
function walkHeaders(dir) {
let out = [];
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) { if (ent.name !== 'detail') out = out.concat(walkHeaders(p)); }
else if (ent.name.endsWith('.hpp')) out.push(p);
}
return out;
}

// A `@see` paragraph holding anything other than `@ref name` items joined by
// commas. The paragraph runs past the `@see` line, as Doxygen's does, until a
// blank line, `*/`, a line opening with any command but `@ref`, or (for a
// `///` comment) a line that is not `///`. Each offending line is reported.
const SEE_LINE = /^(\s*)(\/\/\/)?\s*[@\\]see\b(.*)$/;
const SEE_REF = /[@\\]ref\s+[\w:~<>]+(?:\([^)]*\))?/g;
const NOT_REF_COMMAND = /^[@\\](?!ref\b)\w/;
function scanSeeLines(lines) {
const out = [];
const check = (text, i) => {
const body = text.split('*/')[0];
if (body.replace(SEE_REF, '').replace(/[\s,]/g, '') !== '')
out.push({ line: i + 1, message: `@see entry without @ref renders unlinked: ${body.trim()}` });
return !text.includes('*/');
};
let open = false; // inside a @see paragraph
let slashes = false; // ...that lives in a `///` run
lines.forEach((l, i) => {
const m = SEE_LINE.exec(l);
if (m) {
slashes = m[2] !== undefined;
open = check(m[3], i);
return;
}
if (!open) return;
let text = l;
if (slashes) {
const s = /^\s*\/\/\/(.*)$/.exec(l);
if (!s) { open = false; return; }
text = s[1];
}
const t = text.trim();
if (t === '' || t.startsWith('*/') || NOT_REF_COMMAND.test(t)) { open = false; return; }
open = check(text, i);
});
return out;
}

const findings = { A1: [], A6: [], B2: [], B6: [], SHAPE: [], ANCHOR: [], D2: [] };
const files = walk(PAGES_DIR);

for (const file of files) {
Expand Down Expand Up @@ -300,6 +361,13 @@ for (const file of files) {
}
}

for (const file of walkHeaders(HEADERS_DIR)) {
const rel = path.join('include/boost/capy', path.relative(HEADERS_DIR, file));
for (const s of scanSeeLines(fs.readFileSync(file, 'utf8').split('\n'))) {
findings.B6.push({ file: rel, line: s.line, message: s.message });
}
}

const navText = fs.readFileSync(NAV_FILE, 'utf8');
const topEntries = navText.split('\n').filter(l => /^\* xref:/.test(l));
const qsIndex = topEntries.findIndex(l => /quick-start/.test(l));
Expand Down
48 changes: 47 additions & 1 deletion doc/lint/selftest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,44 @@ try {
'',
].join('\n'));

const d = spawnSync('node', [path.join(SCRIPT_DIR, 'doc-lint.mjs'), DOCLINT_TMP],
// B6 header fixture, passed to doc-lint.mjs as its header tree. Line
// numbers are asserted below: 3, 4 and 5 must fire; 6 and 7 must not.
// detail/ is outside B6's scope, so its bare `@see` must not fire either.
write('hdr/see.hpp', [
'/** Brief.', // 1
'', // 2
' @see task', // 3 <- must fire
' @see @ref task, Executor', // 4 <- must fire (one name unlinked)
' @see @ref run, which describes teardown.', // 5 <- must fire (prose)
' @see @ref task, @ref this_coro::executor', // 6 <- must NOT fire
' @see @ref run(Ex)', // 7 <- must NOT fire
'*/',
'',
].join('\n'));
write('hdr/detail/hidden.hpp', '/** Brief.\n @see task\n*/\n');
// B6 continuation fixture: a `@see` paragraph wraps onto following lines
// until a blank line, `*/`, a non-@ref command, or the end of a `///` run.
// 3 and 12 must fire; nothing else may.
write('hdr/see-wrap.hpp', [
'/** Brief.', // 1
' @see @ref a,', // 2
' b', // 3 <- must fire (bare name, wrapped)
' @see @ref a,', // 4
' @ref b', // 5 <- must NOT fire (wrapped @ref)
' @see @ref a', // 6
'', // 7 blank line ends the paragraph
' Plain prose after the list.', // 8 <- must NOT fire
' @see @ref a', // 9
' @par Note', // 10 a command ends the paragraph
' More prose.', // 11 <- must NOT fire
' @see @ref a, b */', // 12 <- must fire (bare name before */)
'/// @see @ref a,', // 13
'/// @ref b', // 14 <- must NOT fire
'int prose_outside_the_comment;', // 15 <- must NOT fire (/// run ended)
'',
].join('\n'));

const d = spawnSync('node', [path.join(SCRIPT_DIR, 'doc-lint.mjs'), DOCLINT_TMP, path.join(DOCLINT_TMP, 'hdr')],
{ encoding: 'utf8', cwd: DOC_DIR, maxBuffer: 16 * 1024 * 1024 });
check('doc-lint.mjs exits 0 against the B2 fixture tree', d.status === 0,
`exited ${d.status}: ${(d.stderr || '').trim().slice(-200)}`);
Expand Down Expand Up @@ -535,6 +572,15 @@ try {
`attr-anchor.adoc:10 is the passthrough fix and must not fire; got: ${JSON.stringify(anchorRule)}`);
check('ANCHOR finds exactly the one planted prose violation',
anchorRule.length === 1, `expected exactly 1 ANCHOR finding, got: ${JSON.stringify(anchorRule)}`);

// B6: MrDocs renders a bare `@see` name as plain text and warns about
// nothing, so a regression here would be silent on the site too.
const seeLines = dOut.findings.B6.map((f) => `${f.file}:${f.line}`).sort();
check('B6 flags exactly the bare-name, mixed and prose @see lines, nothing in detail/',
JSON.stringify(seeLines) === JSON.stringify(
['include/boost/capy/see-wrap.hpp:12', 'include/boost/capy/see-wrap.hpp:3',
'include/boost/capy/see.hpp:3', 'include/boost/capy/see.hpp:4', 'include/boost/capy/see.hpp:5']),
`got: ${JSON.stringify(seeLines)}`);
}
} finally {
fs.rmSync(DOCLINT_TMP, { recursive: true, force: true });
Expand Down
10 changes: 5 additions & 5 deletions include/boost/capy/buffers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class mutable_buffer;
single-element sequence) and is implicitly convertible to
`const_buffer`.

@see const_buffer, MutableBufferSequence
@see @ref const_buffer, @ref MutableBufferSequence
*/
class mutable_buffer
{
Expand Down Expand Up @@ -130,7 +130,7 @@ class mutable_buffer
single-element sequence). Implicitly constructible from
`mutable_buffer`.

@see mutable_buffer, ConstBufferSequence
@see @ref mutable_buffer, @ref ConstBufferSequence
*/
class const_buffer
{
Expand Down Expand Up @@ -233,7 +233,7 @@ class const_buffer
@li Convertible to `const_buffer`, OR
@li A bidirectional range with value type convertible to `const_buffer`

@see const_buffer, MutableBufferSequence
@see @ref const_buffer, @ref MutableBufferSequence
*/
// tag::const_buffer_sequence_concept[]
template<typename T>
Expand All @@ -258,7 +258,7 @@ concept ConstBufferSequence =
@li Convertible to `mutable_buffer`, OR
@li A bidirectional range with value type convertible to `mutable_buffer`

@see mutable_buffer, ConstBufferSequence
@see @ref mutable_buffer, @ref ConstBufferSequence
*/
// tag::mutable_buffer_sequence_concept[]
template<typename T>
Expand Down Expand Up @@ -474,7 +474,7 @@ length_impl(It first, It last, long)

@return The number of buffers in `bs`.

@see buffer_size
@see @ref buffer_size
*/
template<ConstBufferSequence CB>
std::size_t
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/buffers/buffer_param.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ namespace capy {
@tparam BS The buffer sequence type. Must satisfy either
ConstBufferSequence or MutableBufferSequence.

@see ConstBufferSequence, MutableBufferSequence
@see @ref ConstBufferSequence, @ref MutableBufferSequence
*/
template<class BS, bool MakeConst = false>
requires ConstBufferSequence<BS> || MutableBufferSequence<BS>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/buffers/buffer_slice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ using slice_type = std::conditional_t<
@par !example example


@see slice_type, consuming_buffers
@see @ref slice_type, @ref consuming_buffers
*/
template<class BufferSequence>
requires MutableBufferSequence<BufferSequence>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/buffers/consuming_buffers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ namespace capy {
@par !example example


@see buffer_slice, slice_of
@see @ref buffer_slice
*/
template<class Seq>
requires MutableBufferSequence<Seq> || ConstBufferSequence<Seq>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/context_runnable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ namespace capy {
@par !example example


@see ContextAwaitable, run, run_async
@see @ref ContextAwaitable, @ref run, @ref run_async
*/
// tag::context_runnable_concept[]
template<typename T>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/execution_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ namespace capy {
@par !example example_2


@see Executor, execution_context
@see @ref Executor, @ref execution_context
*/
template<class X>
concept ExecutionContext =
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class execution_context;
@par !example example_2


@see ExecutionContext, execution_context
@see @ref ExecutionContext, @ref execution_context
*/
// tag::executor_concept[]
template<class E>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/read_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ namespace capy {
@par !example example_2


@see ContextAwaitable, MutableBufferSequence, awaitable_decomposes_to
@see @ref ContextAwaitable, @ref MutableBufferSequence, @ref awaitable_decomposes_to
*/
// tag::read_stream_concept[]
template<typename T>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace capy {
@par !example example


@see ReadStream, WriteStream
@see @ref ReadStream, @ref WriteStream
*/
template<typename T>
concept Stream = ReadStream<T> && WriteStream<T>;
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/concept/write_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ namespace capy {
@par !example example_2


@see ContextAwaitable, ConstBufferSequence, awaitable_decomposes_to
@see @ref ContextAwaitable, @ref ConstBufferSequence, @ref awaitable_decomposes_to
*/
// tag::write_stream_concept[]
template<typename T>
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/cond.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace capy {
@par !example example


@see error
@see @ref error
*/
enum class cond
{
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/continuation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class chain_owner;
`h.resume()`. The executor's internal locking provides
the necessary synchronization between these two accesses.

@see Executor, executor_ref
@see @ref Executor, @ref executor_ref
*/
struct continuation
{
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace capy {
@ref cond error conditions instead. These enum values are
implementation details subject to change.

@see cond
@see @ref cond
*/
enum class error
{
Expand Down
Loading
Loading