Skip to content
Open
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
18 changes: 9 additions & 9 deletions .ref-cache.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"http://c2.com/doc/oopsla92.html": 200,
"http://callbackhell.com/": "403 then HTTPError",
"http://callbackhell.com/": "URLError",
"http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html": 200,
"http://disi.unitn.it/~montreso/ds/papers/montresor17.pdf": 200,
"http://dougseven.com/2014/04/17/knightmare-a-devops-cautionary-tale/": 200,
Expand Down Expand Up @@ -2127,7 +2127,7 @@
"https://fp-ts.github.io/core/modules/Either.ts.html": 200,
"https://framework.zend.com/manual/1.12/en/zend.db.table.html": 200,
"https://framework.zend.com/manual/1.12/en/zend.db.table.row.html": 200,
"https://freecontent.manning.com/the-service-locator-anti-pattern/": 202,
"https://freecontent.manning.com/the-service-locator-anti-pattern/": "URLError",
"https://freetype.org/freetype2/docs/reference/ft2-cache_subsystem.html": 200,
"https://fsharp.github.io/fsharp-core-docs/": 200,
"https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-operators.html": 200,
Expand Down Expand Up @@ -4619,7 +4619,7 @@
"https://www.intercom.com/help/en/articles/9515824-what-is-fin": 200,
"https://www.intercom.com/help/en/articles/9929230-the-fin-ai-engine": 200,
"https://www.iso.org/standard/81870.html": 200,
"https://www.iso20022.org/": "TimeoutError",
"https://www.iso20022.org/": "403 then HTTPError",
"https://www.jaegertracing.io/": 200,
"https://www.jaegertracing.io/docs/1.6/architecture/": 200,
"https://www.jaegertracing.io/docs/2.dev/deployment/configuration/": 200,
Expand Down Expand Up @@ -4849,12 +4849,12 @@
"https://www.reactive-streams.org/": 200,
"https://www.research.ed.ac.uk/en/publications/handlers-of-algebraic-effects": 200,
"https://www.research.ed.ac.uk/en/publications/monads-for-functional-programming/": 200,
"https://www.researchgate.net/publication/221366961_Relationship-based_access_control_policies_and_their_policy_languages": "429 then HTTPError",
"https://www.researchgate.net/publication/228342050_Software_design_patterns_for_message_driven_service_oriented_integration_of_stovepipe_applications_in_healthcare_enterprise": "429 then HTTPError",
"https://www.researchgate.net/publication/2378571_Ideal_Hash_Trees": "429 then HTTPError",
"https://www.researchgate.net/publication/2949837_Virtual_Time_and_Global_States_of_Distributed_Systems": "429 then HTTPError",
"https://www.researchgate.net/publication/342799123_RefactoringMiner_20": "429 then HTTPError",
"https://www.researchgate.net/publication/43921655_Combinators_for_bidirectional_tree_transformations_A_linguistic_approach_to_the_view-update_problem": "429 then HTTPError",
"https://www.researchgate.net/publication/221366961_Relationship-based_access_control_policies_and_their_policy_languages": "403 then HTTPError",
"https://www.researchgate.net/publication/228342050_Software_design_patterns_for_message_driven_service_oriented_integration_of_stovepipe_applications_in_healthcare_enterprise": "403 then HTTPError",
"https://www.researchgate.net/publication/2378571_Ideal_Hash_Trees": "403 then HTTPError",
"https://www.researchgate.net/publication/2949837_Virtual_Time_and_Global_States_of_Distributed_Systems": "403 then HTTPError",
"https://www.researchgate.net/publication/342799123_RefactoringMiner_20": "403 then HTTPError",
"https://www.researchgate.net/publication/43921655_Combinators_for_bidirectional_tree_transformations_A_linguistic_approach_to_the_view-update_problem": "403 then HTTPError",
"https://www.rfc-editor.org/info/rfc5246": 200,
"https://www.rfc-editor.org/info/rfc6265/": 200,
"https://www.rfc-editor.org/info/rfc6749": 200,
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.PHONY: check structure refs prose code catalogue catalogue-check by-problem-by-language by-problem-by-language-check duplicates duplicates-test test all stats
.PHONY: check structure refs prose links code catalogue catalogue-check by-problem-by-language by-problem-by-language-check duplicates duplicates-test test all stats

all: check

check: test structure prose code refs catalogue-check by-problem-by-language-check duplicates-test
check: test structure prose links code refs catalogue-check by-problem-by-language-check duplicates-test

structure:
@python3 tools/check-structure.py
Expand All @@ -13,6 +13,9 @@ refs:
prose:
@python3 tools/check-prose.py

links:
@python3 tools/check-links.py --strict

code:
@python3 tools/check-code.py --strict

Expand Down Expand Up @@ -40,6 +43,7 @@ test:
@python3 tools/check-code-test.py
@python3 tools/check-claims-test.py
@python3 tools/next-batch-test.py
@python3 tools/check-links-test.py

duplicates:
@python3 tools/check-duplicates.py --strict
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ level software pattern catalogue. Follow these steps exactly, in order.
2. Read .github/CONTRIBUTING.md, docs/ENTRY-TEMPLATE.md, and one existing
published entry under patterns/ end to end. These are the real, current
rules. Do not assume you already know them.
3. Check docs/AUTHORING-PLAN.md for an unclaimed pattern, or pick a pattern
3. Check docs/PROGRESS.md for an unclaimed pattern, or pick a pattern
the maintainer has not catalogued yet.
4. Create a branch named entry/<slug>, for example entry/circuit-breaker.
Never commit to main.
Expand Down Expand Up @@ -527,7 +527,7 @@ in the repo, the file in the repo wins, not this prompt.

### Authoring plan and progress

See [docs/AUTHORING-PLAN.md](docs/AUTHORING-PLAN.md) for the family by family
See [docs/PROGRESS.md](docs/PROGRESS.md) for the family by family
authoring order and the current state of each family.

## Credits
Expand Down
4 changes: 2 additions & 2 deletions patterns/04-principles-and-laws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Origin. Martin, Larman, Brewer, Conway

42 entries, 327,232 words. Every entry carries all 18
42 entries, 327,229 words. Every entry carries all 18
dimensions from [the entry contract](../../docs/ENTRY-TEMPLATE.md).

## Design Principle
Expand All @@ -13,7 +13,7 @@ dimensions from [the entry contract](../../docs/ENTRY-TEMPLATE.md).
| [Composable](composable.md) | canonical | 7,680 | Every system with more than one moving part eventually needs behavior that no single unit provides on its own. |
| [Dependency Inversion Principle](dependency-inversion-principle.md) | canonical | 7,097 | A codebase grows outward from a small number of policy decisions, what the system does, in what order, and why. |
| [Inversion of Control](inversion-of-control.md) | canonical | 4,789 | In an ordinary, un-inverted call structure, application code owns the entry point. |
| [Predictable](predictable.md) | canonical | 9,475 | A caller who invokes an operation, reads an API's documentation, or pulls a dependency's published version needs to know, before acting, what is going to happen. |
| [Predictable](predictable.md) | canonical | 9,472 | A caller who invokes an operation, reads an API's documentation, or pulls a dependency's published version needs to know, before acting, what is going to happen. |
| [Stable Abstractions Principle](stable-abstractions-principle.md) | canonical | 8,095 | A codebase accumulates two kinds of code over its life. |
| [Unix Philosophy (CUPID)](unix-philosophy-cupid.md) | established | 5,536 | A function, class, module, or service accumulates responsibility over time because adding one more branch to something that already exists is almost always locally cheaper than ... |

Expand Down
8 changes: 4 additions & 4 deletions patterns/04-principles-and-laws/predictable.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ category: Design Principle
aliases: [Predictability, Behavioral Predictability, Deterministic Behavior]
first_described: "Convergent principle with no single coined origin. Earliest formal reification is Bertrand Meyer, Design by Contract, IEEE Computer, October 1992 (contracts as a predictability guarantee); the idempotency reification traces to HTTP method semantics formalized in RFC 2616, 1999, later RFC 7231, 2014"
maturity: canonical
related: [principle-of-least-astonishment, fail-fast, single-source-of-truth, liskov-substitution-principle, postel-law, idempotent-consumer, state, command]
related: [principle-of-least-astonishment, fail-fast, single-source-of-truth, liskov-substitution-principle, postel-law]
incompatible_with: []
verified: 2026-08-02
---
Expand Down Expand Up @@ -771,19 +771,19 @@ converge predictably if the desired state it is converging toward has
exactly one authoritative source, and every out-of-band actor that can also
write to the same state undermines the guarantee.

[Idempotent Consumer](idempotent-consumer.md) is the messaging-system
[Idempotent Consumer](../10-microservices/idempotent-consumer.md) is the messaging-system
counterpart of the idempotency key variant described in dimension 8,
applied to the receiving side of an asynchronous message rather than to a
synchronous request, and the two are frequently implemented with the same
underlying deduplication store.

The [State](state.md) pattern and the closed transition table variant from
The [State](../01-design-patterns-gof/state.md) pattern and the closed transition table variant from
dimension 8 share the same intent, encoding every legal transition
explicitly so an illegal one is structurally impossible or explicitly
rejected rather than silently allowed to happen through an unconstrained
mutation of a status field.

The [Command](command.md) pattern's support for reversible operations, undo
The [Command](../01-design-patterns-gof/command.md) pattern's support for reversible operations, undo
and redo, depends on predictable, forecastable effects. a command's undo
implementation can only reliably reverse an effect it can predict, which
means a command whose execute step touches unpinned non-determinism is
Expand Down
70 changes: 70 additions & 0 deletions tools/check-links-test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Unit tests for check-links.py link validator tool."""

import importlib.util
import tempfile
import unittest
from pathlib import Path

# Load check-links.py module dynamically since file contains a hyphen
TOOLS_DIR = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location(
"check_links", TOOLS_DIR / "check-links.py"
)
check_links_mod = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(check_links_mod)


class TestCheckLinks(unittest.TestCase):
def test_extract_links_from_file_prose_and_code_blocks(self):
content = """# Test Document

Here is a valid link to [README](../README.md).
Here is an external link to [Google](https://google.com).
Here is an anchor link to [#section](#section).

```python
# In code block
a = [1](2)
x = func(arg1, arg2)
```

And another link to [Progress](../../docs/PROGRESS.md#summary).
"""
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".md", delete=False, encoding="utf-8"
) as tmp:
tmp.write(content)
tmp_path = Path(tmp.name)

try:
links = check_links_mod.extract_links_from_file(tmp_path)
targets = [link[2] for link in links]
self.assertIn("../README.md", targets)
self.assertIn("../../docs/PROGRESS.md#summary", targets)
self.assertNotIn("https://google.com", targets)
self.assertNotIn("#section", targets)
self.assertNotIn("2", targets)
finally:
tmp_path.unlink()

def test_check_links_detects_broken(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
valid_file = root / "exists.md"
valid_file.write_text("# Exists", encoding="utf-8")

source_file = root / "source.md"
source_file.write_text(
"Link 1: [Valid](exists.md)\nLink 2: [Broken](missing.md)\n",
encoding="utf-8",
)

broken = check_links_mod.check_links(root)
self.assertEqual(len(broken), 1)
self.assertEqual(broken[0]["target"], "missing.md")
self.assertEqual(broken[0]["source"], "source.md")


if __name__ == "__main__":
unittest.main()
147 changes: 147 additions & 0 deletions tools/check-links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Internal Markdown Relative Link Checker.

Validates that all relative internal links within Markdown files (.md)
point to existing files or directories within the repository.

Ignores:
- External URLs (http://, https://, mailto:, etc.)
- In-page anchors (#anchor)
- Code blocks (fenced ``` or indented code fences)
- Formatting artifacts / false positives in code snippets
"""

from __future__ import annotations

import argparse
import os
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent

# Regex to match Markdown links: [text](link)
LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")


def is_code_block_delimiter(line: str) -> bool:
s = line.strip()
return s.startswith("```") or s.startswith("~~~")


def find_md_files(root_dir: Path) -> list[Path]:
md_files = []
for dirpath, dirnames, filenames in os.walk(root_dir):
# Skip .git and dist directories
rel_dir = os.path.relpath(dirpath, root_dir)
if rel_dir == ".git" or rel_dir.startswith(".git" + os.sep):
continue
if rel_dir == "dist" or rel_dir.startswith("dist" + os.sep):
continue
for filename in filenames:
if filename.endswith(".md"):
md_files.append(Path(dirpath) / filename)
return sorted(md_files)


def extract_links_from_file(filepath: Path) -> list[tuple[int, str, str]]:
"""Returns a list of tuples: (line_number, link_text, link_target) from non-code prose."""
links = []
try:
content = filepath.read_text(encoding="utf-8", errors="replace")
except Exception:
return links

in_code_block = False

for line_idx, line in enumerate(content.splitlines(), start=1):
if is_code_block_delimiter(line):
in_code_block = not in_code_block
continue

if in_code_block:
continue

for m in LINK_RE.finditer(line):
text, target = m.group(1), m.group(2).strip()

# Ignore external links
if (
target.startswith("http://")
or target.startswith("https://")
or target.startswith("mailto:")
or target.startswith("ftp://")
):
continue

# Ignore pure anchors
if target.startswith("#"):
continue

# Ignore false positives with space, asterisks, or commas (typically code snippet artifacts)
if " " in target or "*" in target or "," in target:
continue

links.append((line_idx, text, target))

return links


def check_links(root_dir: Path) -> list[dict]:
broken = []
md_files = find_md_files(root_dir)

for filepath in md_files:
links = extract_links_from_file(filepath)
for line_num, text, target in links:
# Strip anchor fragment if present
path_part = target.split("#")[0]
if not path_part:
continue

# Resolve relative path against file directory
target_path = (filepath.parent / path_part).resolve()

# Check if target exists on disk
if not target_path.exists():
broken.append(
{
"source": str(filepath.relative_to(root_dir)),
"line": line_num,
"text": text,
"target": target,
"resolved_path": str(target_path),
}
)

return broken


def main() -> int:
parser = argparse.ArgumentParser(
description="Check relative internal links in Markdown files."
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit with non-zero code if broken internal links are found.",
)
args = parser.parse_args()

broken = check_links(ROOT)

if broken:
print(f"Found {len(broken)} broken internal relative link(s):")
for b in broken:
print(f" {b['source']}:{b['line']} -> [{b['text']}]({b['target']})")
if args.strict:
return 1
else:
print("All internal relative Markdown links resolve correctly.")

return 0


if __name__ == "__main__":
sys.exit(main())
Loading