Rule pack (lp check)¶
The rule pack is a set of deterministic, read-only checks that run over the item graph and report findings. Rules are SQL+Python only — no LLM, no external network. Hallucinated compliance judgements are a liability, not a feature.
The set is fixed per release: there is no plugin runtime. Adding a rule is a code change, a PR, and a doc update — by design.
Severity levels¶
error— blockslp checkexit code (non-zero if any finding has severity error).warn— surfaced but does not block.info— advisory, hidden below the default severity threshold.
All rules operate only on current heads of supersede chains — historical rows that have been superseded are considered retired and are skipped.
Rules shipped in v0.6¶
missing_rationale — severity error¶
Flags decision items whose rationale is either NULL or under 20
characters after trimming. A decision with no reasoning attached is a
future self asking “why?” with no answer.
Tune the minimum length by editing
src/luplo/core/checks/rules/missing_rationale.py:MIN_LENGTH (not
configurable from .luplo — a team that wants a different floor is
making a durable choice, not a per-project tweak).
undated_retention — severity warn¶
Flags policy items whose title or body mentions PII, retention,
personal data, or personally identifiable, and which have neither
an expires_at timestamp nor a retention_days tag.
The keyword list is small on purpose — growing it is a rule change,
not a config change. A false positive is fixable by adding a
retention_days tag to the policy. A false negative (a real
retention concern the keyword list missed) should prompt adding
the keyword, in code.
dangling_edge — severity warn¶
Flags every link whose target item has been soft-deleted. Soft-deleted items stay in the database for audit, but an active edge pointing at one is almost always stale: either the caller should remove the edge, or the delete should be reversed.
unresolved_conflict — severity warn¶
Flags pairs connected by a conflicts edge where the edge is older
than 30 days and neither side has been superseded. A conflict older
than 30 days that nobody resolved means the team is living with two
rules in force, and someone will eventually guess which one wins.
The 30-day threshold is a constant in
src/luplo/core/checks/rules/unresolved_conflict.py.
unlinked_policy — severity info¶
Flags policy items that no decision references (via any link
type, either direction). Orphan policies are either dead weight or
lore the team applies implicitly but has not recorded. Either way,
the audit trail is missing.
Disabling a rule per project¶
Some rules are not useful in every project. Add the rule’s name to
[checks] disabled_rules in .luplo:
[checks]
disabled_rules = ["undated_retention", "unlinked_policy"]
A rule listed here is skipped even when the caller explicitly asks
for it via lp check --rule NAME. The project-level disable is
the stronger signal.
Invoking¶
CLI¶
# Run every enabled rule, default threshold is warn.
lp check
# Run only specific rules.
lp check --rule missing_rationale --rule dangling_edge
# Include info-level findings.
lp check --severity info
# Only show errors (what CI should do).
lp check --severity error
# List all registered rules and exit.
lp check --list
Exit code is non-zero if any finding has severity error, regardless
of the display threshold.
MCP¶
luplo_check(project_id="myproj", rule="missing_rationale", severity="warn")
Returns markdown. The model may cite 8-character item id prefixes from the output.
HTTP¶
GET /checks?project_id=myproj&rule=missing_rationale
Returns {findings: [...], count: int}. Each finding has
rule_name, severity, message, item_id (nullable), and
details (rule-specific).
What the rule pack is not¶
Not a compliance certification.
lp checkreports signals the human can act on. It does not produce SOC 2 / ISO 27001 evidence.Not an LLM “AI auditor”. Rules are deterministic SQL and a tiny bit of Python. Adding an LLM would swap clarity for hallucination.
Not a plugin runtime. New rules land as code changes to this repository, with tests and docs alongside.