bmm: address PR review on memlog standardization
- Delete orphaned bmad-brainstorming/scripts/tests/test_memlog.py: it
imported the bundled memlog.py removed in this PR (ModuleNotFoundError on
collection) and its unique tests asserted the now-removed status-lifecycle
behavior. The canonical src/scripts/tests/test_memlog.py is the corrected
superset, so no coverage is lost.
- Make runnable memlog command examples self-contained with the full
'uv run {project-root}/_bmad/scripts/memlog.py ... --workspace {doc_workspace}'
form across bmad-brainstorming (converge/finalize/headless), bmad-prd, and
bmad-ux. Terse checklist back-references left short by design.
- bmad-product-brief Update: init .memlog.md if missing (legacy/pre-standard
briefs), matching bmad-prd and bmad-ux; fix the --type override invocation.
This commit is contained in:
parent
aa00389027
commit
ebbb5ccb3b
|
|
@ -30,7 +30,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** A brief the user is proud of, that meets their needs, drawn out through real conversation — do not assume: instead converse and understand, and then help craft the best product brief for their needs. Begin in `## Discovery` before drafting; the brief comes after the picture is on the table. Shape follows the product and need. Treat `{workflow.brief_template}` as a starting structure, not a contract: drop sections that do not earn their place, add sections the product needs, reorder freely - create sections for specialized domains or concerns also as needed. The brief serves the product's story, not the template's shape. Bind `{doc_workspace}` to a fresh folder at `{workflow.brief_output_path}/{workflow.run_folder_pattern}/`, write `brief.md` there with YAML frontmatter (title, status, created, updated), and seed the memlog: `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product>"`. For Update and Validate, `{doc_workspace}` is the existing folder of the brief being targeted.
|
**Create.** A brief the user is proud of, that meets their needs, drawn out through real conversation — do not assume: instead converse and understand, and then help craft the best product brief for their needs. Begin in `## Discovery` before drafting; the brief comes after the picture is on the table. Shape follows the product and need. Treat `{workflow.brief_template}` as a starting structure, not a contract: drop sections that do not earn their place, add sections the product needs, reorder freely - create sections for specialized domains or concerns also as needed. The brief serves the product's story, not the template's shape. Bind `{doc_workspace}` to a fresh folder at `{workflow.brief_output_path}/{workflow.run_folder_pattern}/`, write `brief.md` there with YAML frontmatter (title, status, created, updated), and seed the memlog: `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product>"`. For Update and Validate, `{doc_workspace}` is the existing folder of the brief being targeted.
|
||||||
|
|
||||||
**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). Surface conflicts with prior decisions before changing. Headless override: log the reversal via `memlog.py append --type override`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching.
|
**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). If `.memlog.md` is missing (a legacy or pre-standard brief), init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` first — this update is its first entry. Surface conflicts with prior decisions before changing. Headless override: log the reversal via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type override --text "<reversal + rationale>"`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching.
|
||||||
|
|
||||||
**Validate.** Honest critique against the brief's own purpose. Read the brief, the addendum if present, `.memlog.md`, and any original inputs first — a validation that ignores prior decisions, rejected ideas, or context the user supplied is shallow. Cite specific lines. Caveat what cannot be evaluated. Return inline — no separate file unless asked. Always offer to roll findings into an Update, even in headless mode — include `"offer_to_update": true` in the JSON status block.
|
**Validate.** Honest critique against the brief's own purpose. Read the brief, the addendum if present, `.memlog.md`, and any original inputs first — a validation that ignores prior decisions, rejected ideas, or context the user supplied is shallow. Cite specific lines. Caveat what cannot be evaluated. Return inline — no separate file unless asked. Always offer to roll findings into an Update, even in headless mode — include `"offer_to_update": true` in the JSON status block.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** Bind `{doc_workspace}` to `{workflow.prd_output_path}/{workflow.run_folder_pattern}/`. Write `prd.md` with YAML frontmatter (title, status, created, updated — initial `status: draft`), and seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<PRD/product name>"` so subsequent decisions land in a known file. Tell the user the path. Run `## Discovery`, then `## Finalize`.
|
**Create.** Bind `{doc_workspace}` to `{workflow.prd_output_path}/{workflow.run_folder_pattern}/`. Write `prd.md` with YAML frontmatter (title, status, created, updated — initial `status: draft`), and seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<PRD/product name>"` so subsequent decisions land in a known file. Tell the user the path. Run `## Discovery`, then `## Finalize`.
|
||||||
|
|
||||||
**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, `memlog.py init` it, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `memlog.py append` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`.
|
**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}`, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text "<recovered decision>"` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`.
|
||||||
|
|
||||||
**Validate** (or *analyze*). Critique without changing. Load `references/validate.md`.
|
**Validate** (or *analyze*). Critique without changing. Load `references/validate.md`.
|
||||||
|
|
||||||
|
|
@ -88,5 +88,5 @@ Tell the user the sequence in one sentence, then walk it. Polish goes last so it
|
||||||
4. **Triage open items.** All Open Questions, `[ASSUMPTION]` tags, `[NOTE FOR PM]` callouts. Phase-blockers (would make the PRD unsafe for UX/architecture/epics) surfaced one at a time and resolved; non-blockers deferred with owner + revisit condition logged via `memlog.py append`. If phase-blocker count is high, flag it.
|
4. **Triage open items.** All Open Questions, `[ASSUMPTION]` tags, `[NOTE FOR PM]` callouts. Phase-blockers (would make the PRD unsafe for UX/architecture/epics) surfaced one at a time and resolved; non-blockers deferred with owner + revisit condition logged via `memlog.py append`. If phase-blocker count is high, flag it.
|
||||||
5. **Polish.** Apply `{workflow.doc_standards}` to `prd.md` and `addendum.md` in declared order (structural passes before prose — prose should not polish soon-to-be-cut text). Parallelize across documents, sequential within.
|
5. **Polish.** Apply `{workflow.doc_standards}` to `prd.md` and `addendum.md` in declared order (structural passes before prose — prose should not polish soon-to-be-cut text). Parallelize across documents, sequential within.
|
||||||
6. **External handoffs.** Execute `{workflow.external_handoffs}`; surface returned URLs/IDs. Skip and flag unavailable tools.
|
6. **External handoffs.** Execute `{workflow.external_handoffs}`; surface returned URLs/IDs. Skip and flag unavailable tools.
|
||||||
7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `memlog.py append --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing.
|
7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing.
|
||||||
8. Run `{workflow.on_complete}` if non-empty.
|
8. Run `{workflow.on_complete}` if non-empty.
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** Bind `{doc_workspace}` to `{workflow.ux_output_path}/{workflow.run_folder_pattern}/`. Create `.working/` and `imports/`; seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product/UX>"`; create `DESIGN.md` (frontmatter only) and `EXPERIENCE.md` (frontmatter only). Run Discovery → Finalize.
|
**Create.** Bind `{doc_workspace}` to `{workflow.ux_output_path}/{workflow.run_folder_pattern}/`. Create `.working/` and `imports/`; seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product/UX>"`; create `DESIGN.md` (frontmatter only) and `EXPERIENCE.md` (frontmatter only). Run Discovery → Finalize.
|
||||||
|
|
||||||
**Update.** Read spines + memlog + sources. `memlog.py init` the memlog if missing — this update is entry one. Surface conflicts with prior decisions. Run Finalize.
|
**Update.** Read spines + memlog + sources. If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` — this update is entry one. Surface conflicts with prior decisions. Run Finalize.
|
||||||
|
|
||||||
**Validate.** See `references/validate.md`.
|
**Validate.** See `references/validate.md`.
|
||||||
|
|
||||||
|
|
@ -87,4 +87,4 @@ Outcomes, in order:
|
||||||
- **Key-screen mocks rendered.** Key-screens tool → `.working/` for surfaces where layout drives behavior or anchors visual language.
|
- **Key-screen mocks rendered.** Key-screens tool → `.working/` for surfaces where layout drives behavior or anchors visual language.
|
||||||
- **Mock coverage confirmed.** Walk every IA surface; classify *mocked* vs *spine-only*. Ask: *"These will be built from spine tables alone — any need a visual reference?"* Render more if named; log spine-only choices.
|
- **Mock coverage confirmed.** Walk every IA surface; classify *mocked* vs *spine-only*. Ask: *"These will be built from spine tables alone — any need a visual reference?"* Render more if named; log spine-only choices.
|
||||||
- **Layout extracted, artifacts promoted.** Distill subagent re-reads each `.working/` and `imports/` artifact; lifts visual decisions into DESIGN.md and behavioral decisions into EXPERIENCE.md. Promote `.working/` keepers to `mockups/` (HTML) or `wireframes/` (Excalidraw); imports stay. Inline relative links at relevant spine sections; state spines-win-on-conflict once.
|
- **Layout extracted, artifacts promoted.** Distill subagent re-reads each `.working/` and `imports/` artifact; lifts visual decisions into DESIGN.md and behavioral decisions into EXPERIENCE.md. Promote `.working/` keepers to `mockups/` (HTML) or `wireframes/` (Excalidraw); imports stay. Inline relative links at relevant spine sections; state spines-win-on-conflict once.
|
||||||
- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `memlog.py append --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`.
|
- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Pick by what the decision needs:
|
||||||
- **PMI (Plus / Minus / Interesting)** — when one strong candidate needs pressure-testing before commitment: list its pluses, minuses, and the merely-interesting, then judge.
|
- **PMI (Plus / Minus / Interesting)** — when one strong candidate needs pressure-testing before commitment: list its pluses, minuses, and the merely-interesting, then judge.
|
||||||
- **MoSCoW** — when scoping a build: sort into Must / Should / Could / Won't-this-time.
|
- **MoSCoW** — when scoping a build: sort into Must / Should / Could / Won't-this-time.
|
||||||
|
|
||||||
Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --type decision --text "<one-line gist>"` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing.
|
Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text "<one-line gist>"` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing.
|
||||||
|
|
||||||
## Then finalize
|
## Then finalize
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ In Facilitator mode this is the one place your own creative contribution is welc
|
||||||
1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier, not just the recent obvious ones (in Creative Partner mode the `(... by user)` tags tell you which were theirs). Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point.
|
1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier, not just the recent obvious ones (in Creative Partner mode the `(... by user)` tags tell you which were theirs). Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point.
|
||||||
2. **Then add the connections they would miss.** Lean in creatively — not new raw ideas, but the non-obvious links: this idea from technique one quietly solves that tension from technique four; these three are one idea wearing three hats; this wildcard is the real breakthrough.
|
2. **Then add the connections they would miss.** Lean in creatively — not new raw ideas, but the non-obvious links: this idea from technique one quietly solves that tension from technique four; these three are one idea wearing three hats; this wildcard is the real breakthrough.
|
||||||
|
|
||||||
Record the insights and chosen directions with `memlog.py append --type insight`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below.
|
Record the insights and chosen directions with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text "<insights + chosen directions>"`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below.
|
||||||
|
|
||||||
## Artifacts
|
## Artifacts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ Free-form structured payload in the first message; provide what applies:
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
|
1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
|
||||||
2. Run the divergent session per **The inversion**, capturing each idea with `memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `memlog.py append --type technique --text "started <name>"`.
|
2. Run the divergent session per **The inversion**, capturing each idea with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type technique --text "started <name>"`.
|
||||||
3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `memlog.py append --type insight`, then run `memlog.py set --workspace {doc_workspace} --key status --value complete`.
|
3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text "<insights>"`, then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`.
|
||||||
4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.)
|
4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.)
|
||||||
5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty.
|
5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.10"
|
|
||||||
# dependencies = ["pytest>=8.0"]
|
|
||||||
# ///
|
|
||||||
"""Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py
|
|
||||||
|
|
||||||
The spine under test is the flat, append-only, chronological invariant: every entry is
|
|
||||||
one line recorded at the end in the order it happened — no sections, no grouping.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
||||||
import memlog # noqa: E402
|
|
||||||
|
|
||||||
MEMLOG = ".memlog.md"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ws(tmp_path):
|
|
||||||
return str(tmp_path)
|
|
||||||
|
|
||||||
|
|
||||||
def read(ws):
|
|
||||||
return (Path(ws) / MEMLOG).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def body_of(ws):
|
|
||||||
return memlog.split(read(ws))[1]
|
|
||||||
|
|
||||||
|
|
||||||
def entries(ws):
|
|
||||||
return [ln for ln in body_of(ws).splitlines() if ln.startswith("- ")]
|
|
||||||
|
|
||||||
|
|
||||||
def init(ws, **fields):
|
|
||||||
fields = fields or {"topic": "Reinvent the lunchbox", "goal": "ideas for a pitch"}
|
|
||||||
argv = ["init", "--workspace", ws]
|
|
||||||
for k, v in fields.items():
|
|
||||||
argv += ["--field", f"{k}={v}"]
|
|
||||||
assert memlog.main(argv) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def append(ws, text, entry_type=None, by=None):
|
|
||||||
argv = ["append", "--workspace", ws, "--text", text]
|
|
||||||
if entry_type:
|
|
||||||
argv += ["--type", entry_type]
|
|
||||||
if by:
|
|
||||||
argv += ["--by", by]
|
|
||||||
assert memlog.main(argv) == 0
|
|
||||||
|
|
||||||
|
|
||||||
# --- init ---------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_init_writes_frontmatter_fields(ws):
|
|
||||||
init(ws)
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "Reinvent the lunchbox"
|
|
||||||
assert meta["goal"] == "ideas for a pitch"
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
assert "updated" in meta
|
|
||||||
assert body.strip() == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_arbitrary_fields(ws):
|
|
||||||
init(ws, topic="T", audience="board")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert meta["audience"] == "board"
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_refuses_overwrite(ws):
|
|
||||||
init(ws)
|
|
||||||
assert memlog.main(["init", "--workspace", ws, "--field", "topic=other"]) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_creates_missing_workspace(tmp_path):
|
|
||||||
nested = str(tmp_path / "a" / "b")
|
|
||||||
assert memlog.main(["init", "--workspace", nested, "--field", "topic=T"]) == 0
|
|
||||||
assert (Path(nested) / MEMLOG).is_file()
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_rejects_malformed_field(ws):
|
|
||||||
assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2
|
|
||||||
|
|
||||||
|
|
||||||
# --- append: flat chronological order is the whole point -----------------
|
|
||||||
|
|
||||||
def test_append_lands_at_end_in_order(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "first")
|
|
||||||
append(ws, "second")
|
|
||||||
append(ws, "third")
|
|
||||||
assert entries(ws) == ["- first", "- second", "- third"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_sections_or_headings_ever(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "started foo", entry_type="technique")
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
append(ws, "started bar", entry_type="technique")
|
|
||||||
assert "## " not in body_of(ws) # the flat log never grows headings
|
|
||||||
|
|
||||||
|
|
||||||
def test_type_renders_as_inline_tag(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "the earth revolves around the sun", entry_type="idea")
|
|
||||||
append(ws, "how do we handle stampede?", entry_type="question")
|
|
||||||
body = body_of(ws)
|
|
||||||
assert "- (idea) the earth revolves around the sun" in body
|
|
||||||
assert "- (question) how do we handle stampede?" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_without_type_is_plain_note(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "bare entry")
|
|
||||||
assert entries(ws) == ["- bare entry"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_collapses_newlines_into_one_line(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "line one\nline two\n spaced out")
|
|
||||||
assert entries(ws) == ["- line one line two spaced out"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_revisited_technique_is_just_a_later_entry(ws):
|
|
||||||
# the user's model: switching techniques is an entry, not a section to return to
|
|
||||||
init(ws)
|
|
||||||
append(ws, "started SCAMPER", entry_type="technique")
|
|
||||||
append(ws, "magnetic latch", entry_type="idea")
|
|
||||||
append(ws, "started Six Hats", entry_type="technique")
|
|
||||||
append(ws, "stale data risk", entry_type="idea")
|
|
||||||
append(ws, "started SCAMPER", entry_type="technique") # back to SCAMPER — just appended again
|
|
||||||
append(ws, "stackable tiers", entry_type="idea")
|
|
||||||
assert entries(ws) == [
|
|
||||||
"- (technique) started SCAMPER",
|
|
||||||
"- (idea) magnetic latch",
|
|
||||||
"- (technique) started Six Hats",
|
|
||||||
"- (idea) stale data risk",
|
|
||||||
"- (technique) started SCAMPER",
|
|
||||||
"- (idea) stackable tiers",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_by_renders_attribution_in_tag(ws):
|
|
||||||
# Creative Partner mode must record whose idea each one was
|
|
||||||
init(ws)
|
|
||||||
append(ws, "magnetic latch lid", entry_type="idea", by="user")
|
|
||||||
append(ws, "lid doubles as a plate", entry_type="idea", by="coach")
|
|
||||||
body = body_of(ws)
|
|
||||||
assert "- (idea by user) magnetic latch lid" in body
|
|
||||||
assert "- (idea by coach) lid doubles as a plate" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_by_without_type_renders_alone(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "off-the-cuff thought", by="coach")
|
|
||||||
assert entries(ws) == ["- (by coach) off-the-cuff thought"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_heterogeneous_entry_types_coexist(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
append(ws, "an open question", entry_type="question")
|
|
||||||
append(ws, "a decision we made", entry_type="decision")
|
|
||||||
append(ws, "user wants mobile-first", entry_type="direction")
|
|
||||||
body = body_of(ws)
|
|
||||||
for tag in ("(idea)", "(question)", "(decision)", "(direction)"):
|
|
||||||
assert tag in body
|
|
||||||
|
|
||||||
|
|
||||||
# --- set ----------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_set_flips_status(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
||||||
assert memlog.split(read(ws))[0]["status"] == "complete"
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_preserves_body(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "keep me", entry_type="idea")
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["status"] == "complete"
|
|
||||||
assert "- (idea) keep me" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_can_add_new_field(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
||||||
assert memlog.split(read(ws))[0]["owner"] == "BMad"
|
|
||||||
|
|
||||||
|
|
||||||
def test_updated_stays_last(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
||||||
meta = memlog.split(read(ws))[0]
|
|
||||||
assert list(meta)[-1] == "updated"
|
|
||||||
|
|
||||||
|
|
||||||
# --- robustness ---------------------------------------------------------
|
|
||||||
|
|
||||||
def test_roundtrip_render_is_stable(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "one", entry_type="idea")
|
|
||||||
first = read(ws)
|
|
||||||
meta, body = memlog.split(first)
|
|
||||||
assert memlog.render(meta, body) == first
|
|
||||||
|
|
||||||
|
|
||||||
def test_commas_in_field_survive(ws):
|
|
||||||
init(ws, topic="cars, trains, and planes")
|
|
||||||
append(ws, "z", entry_type="idea")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "cars, trains, and planes"
|
|
||||||
|
|
||||||
|
|
||||||
def test_triple_dash_in_field_does_not_corrupt_frontmatter(ws):
|
|
||||||
# A `---` inside a value must NOT be read as the closing fence: topic stays intact,
|
|
||||||
# status survives, and the body never leaks frontmatter text.
|
|
||||||
init(ws, topic="Pricing --- tiers --- and add-ons")
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "Pricing --- tiers --- and add-ons"
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
assert entries(ws) == ["- (idea) an idea"]
|
|
||||||
assert "status:" not in body # frontmatter never bled into the body
|
|
||||||
|
|
||||||
|
|
||||||
def test_triple_dash_status_survives_in_ack(ws, capsys):
|
|
||||||
init(ws, topic="a --- b")
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["status"] == "active" # not "" — frontmatter recovered cleanly
|
|
||||||
|
|
||||||
|
|
||||||
def test_newline_in_field_is_neutralized(ws):
|
|
||||||
# A value carrying a newline can't break the fence on the next round-trip.
|
|
||||||
memlog.main(["init", "--workspace", ws, "--field", "topic=line one\nline two"])
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert "\n" not in meta["topic"]
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_emits_json_ack(ws, capsys):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["ok"] is True
|
|
||||||
assert out["status"] == "active"
|
|
||||||
assert out["entries"] == 1
|
|
||||||
assert out["memlog"].endswith(MEMLOG)
|
|
||||||
assert "section" not in out # sections are gone
|
|
||||||
|
|
||||||
|
|
||||||
def test_ack_entry_count_climbs(ws, capsys):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "a")
|
|
||||||
append(ws, "b")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["entries"] == 2
|
|
||||||
Loading…
Reference in New Issue