Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
144
planned
1,362
shipped

Motir · Roadmap

MOTIR-4007Done

lib/legal/documents.ts becomes a CONFIGURED manifest reader — the re-consent gate keeps working with no content/legal/ in the tree

Opened by Zhu Yue ·

Type: code · Executor: coding_agent · Repo: motir-core · ONE PR.

lib/legal/documents.ts is the ONE place that reads content/legal/, and it feeds two things: the pages that are leaving, and the re-consent gate that is staying. This card changes its SOURCE from the filesystem to the configured manifest the decision card settles, so the gate keeps working with nothing in content/.

What the module does today, verified on origin/main e76d1afdd

LEGAL_DIR = join(process.cwd(), 'content', 'legal'); listLegalDocuments() is a readdirSync + one readFileSync per .md; parseLegalDocument splits front matter into { slug, title, version, effectiveDate, status, changeSummary, body }, mapping TBD and '' to a null effectiveDate; getLegalDocument matches against the DIRECTORY LISTING rather than building a path, which is what makes ../../etc/passwd return null; byPreferredOrder sorts by PREFERRED_ORDER then slug; legalDocumentSlugs maps the list.

⚠️ AMENDED 2026-09-01 by MOTIR-4004PREFERRED_ORDER AND byPreferredOrder ARE REMOVED, and the manifest's own array ORDER is authoritative. docs/decisions/public-surface-hosts.md AMENDMENT 2 §C decided it: the constant existed because a directory listing has no order, an authored array does, and a hardcoded list in the open product re-sorting an operator's manifest imposes moooon's document ordering on every self-hoster — a smaller instance of exactly what this story is undoing. So the criterion below listing byPreferredOrder among the unchanged exports is superseded: it goes, with its constant. Nothing else about the exported surface changes, and no consumer imports it (git grep -n byPreferredOrder — its only non-test reader is listLegalDocuments itself).

The manifest's SHAPE and its VALIDATION are also decided there and are not open questions for this card: one env value, MOTIR_LEGAL_DOCUMENTS, holding a JSON array of { slug, title, version, effectiveDate, changeSummary, url }; an entry that does not validate is rejected per entry AND loudly — an error log naming the slug and the failing field, a health/preflight surface reporting the manifest as faulted rather than unconfigured, and a separately named condition when the rejected slug is one of RECONSENT_DOCUMENT_SLUGS. Silent rejection is explicitly rejected there.

Its non-test callers, and what each takesgit grep -n "listLegalDocuments\|getLegalDocument" origin/main -- ':!lib/legal/documents.ts':

callerfields useddisposition
lib/services/legalAcceptanceService.ts:34slug, versionstays — re-pointed at the manifest by this card
app/(auth)/re-consent/page.tsx:63slug, title, version only, and it already handles nullstays, unchanged in shape
app/(public)/legal/page.tsx · [slug]/page.tsxeverything including bodyleavesthe deletion card's

No surviving caller reads body. That is what makes this a source swap rather than a redesign.

The change

  1. lib/legal/documents.ts reads the manifest. Keep every export — LegalDocument, listLegalDocuments, getLegalDocument, legalDocumentSlugs, byPreferredOrder — so no consumer's import changes. LegalDocument gains url and loses body; parseLegalDocument and splitFrontMatter go with the files they parsed.
  2. Parse and VALIDATE the manifest once, per the ADR's chosen shape and its chosen failure behaviour. This is the load-bearing half, because a malformed entry does not degrade to absent: parseSemanticVersion returns null for an unreadable version and isMaterialChange then answers true, which holds every signed-in reader at /re-consent. An entry that does not validate must not reach listLegalDocuments().
  3. Unset ⇒ []. Every downstream behaviour for the empty list already exists and is documented: recordAcceptance's "NO EMPTY-SET GUARD HERE, DELIBERATELY", outstandingReconsent's [], and re-consent/page.tsx's terms ? … : null. Do not add a second guard at the service tier — that comment says why.
  4. lib/legal/consent.ts is UNCHANGED. It is pure, it takes documents as an argument, and RECONSENT_DOCUMENT_SLUGS stays the closed three-slug list for the reason its own header gives. Adding a document to the manifest must still not silently start gating the product on it.
  5. lib/services/legalAcceptanceService.ts keeps its read-at-call-time contract. Its comment argues against a module-level cache because a cache serves the previous version of the Terms for the life of a process; a manifest read is cheaper than the readdirSync it replaces, so the argument holds and the shape does not change.
  6. PREFERRED_ORDER's disposition is decided and stated. Either the manifest's own order is authoritative (drop the constant) or the constant still sorts it (keep it, and say why a manifest author's order is not trusted). Do not leave both.

⚠️ The coverage floors are 100% and they are in the config already

vitest.config.ts pins per-file thresholds on lib/legal/consent.ts, lib/legal/documents.ts, lib/legal/reconsentGate.ts, lib/repositories/legalAcceptanceRepository.ts and lib/services/legalAcceptanceService.ts. Rewriting documents.ts means its new branches — a missing manifest, a malformed entry, an unknown slug, an entry with no effective date — are all covered in this PR. byPreferredOrder is exported and pure precisely so its unknown-slug branches are reachable from a unit test; keep that property whatever the constant's disposition.

Boundary

  • It changes no rendered surface. Sign-up, the rail and the re-consent rows are the link-surfaces card's; this card's url field is what that one consumes.
  • It deletes nothing. content/legal/ and app/(public)/legal/ are still in the tree when this merges — the pages simply stop being the loader's reason to exist. The deletion card removes them.
  • It does not touch lib/legal/consent.ts or reconsentGate.ts.
  • It does not provision anything. The cloud deployment's manifest value is the provisioning card's.

Acceptance criteria

  • lib/legal/documents.ts reads the configured manifest and touches no filesystem; git grep -n "content/legal\|readdirSync\|LEGAL_DIR" lib/legal/documents.ts returns nothing.
  • Its exported surface is unchanged in NAME and call shape — listLegalDocuments, getLegalDocument, legalDocumentSlugs, byPreferredOrder, LegalDocument — so no consumer's import line changes; LegalDocument carries url and no longer carries body.
  • With no manifest configured listLegalDocuments() returns [], getLegalDocument(<any>) returns null, and a unit test asserts each.
  • A malformed entry never reaches a consumer: a test drives an entry whose version does not match ^\d+\.\d+\.\d+$ and asserts the ADR's chosen behaviour, naming the alternative that was rejected.
  • getLegalDocument still returns null for a slug that names no entry, including a traversal-shaped one — the existing '../../package' and '../../../etc/passwd' cases in tests/legal/legalDocuments.test.ts stay green.
  • legalAcceptanceService.recordAcceptance and resolveOutstanding behave identically over a manifest of the same three documents as over today's files — asserted against real Postgres, not a stub.
  • No second empty-set guard is added at the service tier, and the PR body quotes the comment that says why.
  • PREFERRED_ORDER's disposition is stated in the module and in the PR body.
  • The five files vitest.config.ts pins are at their configured thresholds; the PR body shows the coverage run.
  • No test is deleted because its subject moved: a test covering front-matter PARSING moves with the parser to the motir.co card or is removed with its reason stated in the PR body.

Context refs

  • motir-core/lib/legal/documents.ts — the module being rewritten
  • motir-core/lib/legal/consent.ts — unchanged; read its RECONSENT_DOCUMENT_SLUGS header for why the list stays closed
  • motir-core/lib/services/legalAcceptanceService.tsreconsentDocuments() and the no-cache argument
  • motir-core/app/(auth)/re-consent/page.tsx — the second surviving caller; takes slug/title/version and already handles null
  • motir-core/vitest.config.ts — the per-file coverage thresholds on the five legal files
  • motir-core/tests/legal/legalDocuments.test.ts — the traversal and ordering cases that must survive
  • motir-core/CLAUDE.md — the layering contract
  • the decision card — the manifest shape and the validation behaviour; blocked_by

Advisory dispositions

  • likely-missing-edgethe motir.co card: NOT REAL, and the edge would INVERT the order. This card names that one as the recipient of the front-matter parser it retires, not as a producer it reads. Nothing here consumes anything that card ships; the hand-off runs the other way, and that card is blocked_by its own design rather than by this one.
  • The remaining reference advisories name cards this one's Boundary excludes. A boundary exists to say a deliverable is somebody else's, and naming the owner is what makes the exclusion checkable — so the reference is deliberate and no edge is owed.

Discussion

No comments yet.

Adding to this discussion signs you in on app.motir.co and brings you back to this request.

Add a comment