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
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

## Unreleased

### Fixed

- Resolved review feedback on PR #5: revised release runbook bullets for
clarity, enforced phase heading order in tests, and clarified
commitment and signpost boundedness invariants.

### Added

- Adopted the "System-Style JavaScript" standard as repo doctrine,
documenting core principles like runtime truth and hexagonal
architecture in `docs/method/process.md`.
- Hardened domain models in `src/domain.ts` using Zod for runtime
validation, ensuring boundary data is honest and core logic is
browser-portable.
- Added a formal Git branch and workflow policy in `docs/method/process.md`,
defining naming conventions (`####-slug`, `maint-slug`) and the
"Ship Sync Maneuver" for signpost maintenance.
- Implemented a GitHub Issue Adapter (`method sync github`) that
synchronizes backlog items to GitHub issues and persists IDs in
YAML frontmatter.
- Formalized the "Executive Summary Protocol" in `docs/method/process.md`
as a repeatable, 4-phase synthesis workflow.
- Implemented a Model Context Protocol (MCP) server (`method mcp`) to
expose METHOD tools to external agents programmatically.
- Extracted a clean, programmable `Method` API surface in `src/index.ts`,
decoupling domain logic from CLI presentation.
- Standardized YAML frontmatter across all document classes (Design,
Retro, Backlog, Signposts) with automated enforcement in the test
suite.
- Refreshed `docs/VISION.md` with trusted provenance metadata and a
source manifest covering eight completed cycles.
- Added a `drift` command to detect playback-question drift in active
cycles.
- Added invariants as a first-class METHOD concept: named properties
that must remain true across all cycles, defined in
`docs/invariants/<name>.md`. Legends now exist to guard invariants,
giving them a concrete job beyond organizing attention. This repo's
four invariants: cycle-traceability, commitment-integrity,
signpost-provenance, and signpost-boundedness.
- Added a minimal GitHub Actions CI gate that runs `npm ci`,
`npm run build`, and `npm test` on `push` and `pull_request`, pinned
to `ubuntu-24.04` with Node `22`.
Expand Down
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Witnesses are not victory photos. They are rerunnable proof.

```text
docs/
invariants/
<name>.md properties that must remain true
method/
backlog/
inbox/ raw ideas, anyone, anytime
Expand All @@ -70,10 +72,15 @@ docs/
*.md everything else
legends/ named domains
retro/<cycle>/<task>.md retrospectives
releases/vX.Y.Z/ internal release packets
graveyard/ rejected ideas
guide.md operator advice and non-doctrinal practice notes
process.md how cycles run
release.md how releases work
release-runbook.md sequential release pre-flight
releases/
vX.Y.Z.md user-facing release notes and migration guides
README.md release note structure
design/
<cycle>/<task>.md cycle design docs
*.md living documents
Expand All @@ -83,6 +90,9 @@ Repo signposts live at root or one level into `docs/`. `README.md` is
the standing root exception; every other signpost uses `ALL_CAPS.md`.
Deeper than that, it is not a signpost.

Release notes live under `docs/releases/`, and internal release packets
live under `docs/method/releases/`.

---

## Signposts
Expand Down Expand Up @@ -175,12 +185,38 @@ Same loop regardless:

---

## Invariants

A named property that must remain true across all cycles. Invariants
live in `docs/invariants/<name>.md`. Each one states the property, why
it matters, and how to check whether it still holds.

Invariants are local to the repo. Each project discovers its own.
A repo with no invariants yet is normal - they surface as you learn
what actually breaks when it drifts.

An invariant file should answer:

1. **What must remain true?** - one sentence.
2. **Why does it matter?** - what breaks if it drifts.
3. **How do you check?** - the concrete test, query, or inspection.

Invariants give legends their job. A legend without an invariant is
just an area of attention. A legend guarding an invariant has a
standing question: did this cycle preserve it?

---

## Legends

A named domain that spans many cycles. Legends organize attention, not
timelines - they are reference frames, not milestones. A legend never
starts or finishes. It describes what it covers, who cares, what
success looks like, and how you know.
starts or finishes. It describes what it covers, what invariants it
guards, what success looks like, and how you know.

A legend's standing playback questions should ask whether its
invariants held. This is what makes a legend load-bearing: not the
backlog items it covers, but the properties it protects.

A legend code (for example, `PROCESS` or `SYNTH`) prefixes backlog filenames so
that `ls` reveals domain load at a glance. Legends live in
Expand All @@ -189,10 +225,12 @@ that `ls` reveals domain load at a glance. Legends live in
The current legends in this repo are:

- `PROCESS` - METHOD's own mechanics: cycle discipline, backlog
operations, drift detection, and named work patterns.
operations, drift detection, and named work patterns. Guards
**cycle-traceability** and **commitment-integrity**.
- `SYNTH` - repo-wide synthesis and signposts: executive summaries,
generated signpost provenance, and the boundary between artifact
history and semantic provenance.
history and semantic provenance. Guards **signpost-provenance** and
**signpost-boundedness**.

Not every METHOD repo needs these exact legends. Legends are local to
the repo and should reflect the domains that actually organize its
Expand Down
144 changes: 144 additions & 0 deletions backfill_frontmatter.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const fs = require('fs');
const path = require('path');

const REPO_ROOT = process.cwd();
const DOCS_DIR = path.join(REPO_ROOT, 'docs');

const EXCLUDED_FILES = [
'docs/BEARING.md',
'docs/VISION.md',
'docs/method/process.md',
'docs/method/release.md',
'docs/method/release-runbook.md',
'docs/method/releases/README.md',
'docs/releases/README.md',
];

const EXCLUDED_DIRS = [
'docs/method/legends'
];

function getAllMarkdownFiles(dir, allFiles = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const relativePath = path.relative(REPO_ROOT, fullPath);

if (fs.statSync(fullPath).isDirectory()) {
if (!EXCLUDED_DIRS.some(d => relativePath === d || relativePath.startsWith(d + '/'))) {
getAllMarkdownFiles(fullPath, allFiles);
}
} else if (file.endsWith('.md')) {
if (!EXCLUDED_FILES.includes(relativePath)) {
allFiles.push(fullPath);
}
}
}
return allFiles;
}

const markdownFiles = getAllMarkdownFiles(DOCS_DIR);

for (const filePath of markdownFiles) {
const relativePath = path.relative(REPO_ROOT, filePath);
let content = fs.readFileSync(filePath, 'utf8');

// If it already has frontmatter, we might need to fix it if we just wrote it wrong
// But let's just re-process everything that doesn't look like "original" docs
// Actually, I'll just check if it has the title in frontmatter.

let title = '';
let body = content;

if (content.startsWith('---\n')) {
const endMatch = content.indexOf('\n---\n', 4);
if (endMatch !== -1) {
const fmContent = content.substring(4, endMatch);
const titleMatch = fmContent.match(/^title:\s+"(.*)"$/m);
if (titleMatch) {
title = titleMatch[1];
}
body = content.substring(endMatch + 5).trim();
}
} else {
// Match only the first line if it's a heading
const lines = content.split('\n');
if (lines[0].startsWith('# ')) {
title = lines[0].substring(2).trim();
} else {
const titleMatch = content.match(/^#\s+(.*)$/m);
if (titleMatch) {
title = titleMatch[1].trim();
}
}
}

const frontmatter = {};
if (title) {
frontmatter.title = title;
}

// Determine type and extract fields
if (relativePath.startsWith('docs/design/')) {
const legendMatch = content.match(/^Legend:\s+(.*)$/m);
frontmatter.legend = legendMatch ? legendMatch[1].trim() : 'none';
} else if (relativePath.startsWith('docs/method/retro/') && !relativePath.includes('/witness/')) {
if (frontmatter.title) {
frontmatter.title = frontmatter.title.replace(/\s+Retro$/, '');
}
const outcomeMatch = content.match(/^Outcome:\s+(.*)$/m);
frontmatter.outcome = outcomeMatch ? outcomeMatch[1].trim() : '';
const driftCheckMatch = content.match(/^Drift check:\s+(.*)$/m);
frontmatter.drift_check = driftCheckMatch ? driftCheckMatch[1].trim() : '';
} else if (relativePath.startsWith('docs/method/backlog/')) {
const fileName = path.basename(filePath);
const prefixMatch = fileName.match(/^([A-Z]+)_/);
frontmatter.legend = prefixMatch ? prefixMatch[1] : 'untagged';
}

// Construct YAML string
let yamlStr = '---\n';
const keys = Object.keys(frontmatter);
if (keys.includes('title')) {
yamlStr += `title: ${JSON.stringify(frontmatter.title)}\n`;
}
for (const key of keys) {
if (key === 'title') continue;
// Don't quote outcome, drift_check, legend unless they have spaces
const value = frontmatter[key];
if (value.includes(' ') || key === 'title') {
yamlStr += `${key}: ${JSON.stringify(value)}\n`;
} else {
yamlStr += `${key}: ${value}\n`;
}
}
yamlStr += '---\n\n';

// Cleanup body
let newBody = body;

// Remove first heading if still there
const titleMatch = body.match(/^#\s+(.*)$/m);
if (titleMatch) {
newBody = newBody.replace(titleMatch[0], '').trim();
}

if (relativePath.startsWith('docs/design/')) {
const legendMatch = body.match(/^Legend:\s+(.*)$/m);
if (legendMatch) {
newBody = newBody.replace(legendMatch[0], '').trim();
}
} else if (relativePath.startsWith('docs/method/retro/') && !relativePath.includes('/witness/')) {
const outcomeMatch = body.match(/^Outcome:\s+(.*)$/m);
if (outcomeMatch) {
newBody = newBody.replace(outcomeMatch[0], '').trim();
}
const driftCheckMatch = body.match(/^Drift check:\s+(.*)$/m);
if (driftCheckMatch) {
newBody = newBody.replace(driftCheckMatch[0], '').trim();
}
}

fs.writeFileSync(filePath, yamlStr + newBody + '\n');
console.log(`Processed ${relativePath}`);
}
31 changes: 19 additions & 12 deletions docs/BEARING.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
---
title: "BEARING"
legend: none
---

# BEARING

This signpost summarizes direction. It does not create commitments or
replace backlog items, design docs, retros, or CLI status.

## Where are we going?

Current priority: pull `PROCESS_cli-module-split` and turn the CLI
entry point back into a thin shell around smaller runtime-owned modules.
Current priority: pull `PROCESS_behavior-spike-convention` to finalize
the repo's pattern vocabulary, then pivot toward a maintenance cycle
to re-evaluate the deep backlog in light of the new system maturity.

## What just shipped?

`0006-ci-gates` - the repo now has a minimal CI gate on GitHub Actions,
running `npm ci`, `npm run build`, and `npm test` on `ubuntu-24.04`
with Node `22`.
- `0016-system-style-javascript-adoption`: Adopted the "System-Style JS"
standard and hardened domain models with Zod.
- `0015-git-branch-workflow-policy`: Defined branch naming conventions
and the "Ship Sync Maneuver."
- `0014-github-issue-adapter`: Added `method sync github` to project
backlog state to GitHub Issues.

## What feels wrong?

- `src/cli.ts` is still carrying too many concerns at once, which makes
review and future cycle work harder than it should be.
- Generated signposts are still only partially formalized: provenance is
defined, but generated-file markers and regeneration guidance are not
part of the shipped contract yet.
- Review state still lives outside METHOD's repo-native coordination
surface; branch and PR context carry that truth for now.
- Backlog lanes are getting deep; we need a maintenance cycle to
re-evaluate `cool-ideas` and `up-next` in light of the new MCP/API
capabilities.
- We have the doctrine for "Ship Sync," but the maneuver itself is
still manual and error-prone.
Loading
Loading