diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd4957..8f107cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- new(brd-report): the Stage 2 report opens with the pass-1 process map (drawn by `process-map.js build --embed`, not a copy) and lists requirements and open questions under it, each tied to its step; a click on a step filters the list, what no step covers stays under "whole app". `open-questions.html` and `bin/questions-report.sh` are retired: the gate and the interview hook write `analysis/brd-report.html`, and an old project's `docs/open-questions.html` is never an error, only pointed away from. Stage 3: the prototype links to the full map and the map links back — a business owner could not tell where in the work a question fits (field run, 2026-10-16) — MendixMau + - learn(multilingual-app): **a second language is four decisions the translations never see — source language, enabled languages, the user's starting language, and a switch a normal role can use.** New `skills/learned-multilingual-app.md` (routed on demand, stages 3/5/6, architect, mdl + review). Field run 2026-10-09 (Mendix 11.15.0, mxcli v0.25.0, an app authored in Spanish gaining ES + PT): 418 strings per language written correctly, and still the language picker raised an error, showed a blank label and changed nothing, with check, exec.sh and lint green. Causes: captions authored under the default `en_US` (so `en_US` is the authored language); demo users on that hidden source language; the Marketplace LanguageSelector writing `System.User_Language` from the browser, which this project's non-admin role could not, with no grant route found (mxcli `check` passes the grant, `exec` refuses it; Studio Pro does not offer the member, so the first fix — a Studio Pro hand step — was impossible; a probe on the `System.User` module role now comes first); `login.html` outside the translation system. Recipe: a microflow sets `$currentUser`'s language, a nanoflow reloads, CSS highlights from ``; seeded users start in a visible language. `learned-mdl-cannot-express.md` gains the LanguageSelector row. — CarlesZ - process(inbox): **five field notes from an existing-app legacy-widget rebuild** — exec.sh keeps an MDL write when mxbuild crashes on it and the next exec runs on "Baseline NOT measured"; a signal stop of `run --test-endpoint` leaves AfterStartupMicroflow on `MxTest.RegisterEndpoint` (second field case for #244); Unit-row delete refused as ORPHAN, rename-then-drop works; a durable-rebuild kit with a "restate from DESCRIBE" STOP row (DESCRIBE omits module-role and layout-row visibility: 97 role locks in 16 documents, none shown); learned entries for File Uploader image mode (CE0463), native-mapped `radiobuttons`/`image`, tab-page `CaptionParams`/`Visible:` silently dropped, BUG-39 on widget XPath, and two-hop data-source paths. Three further notes (restore-mpr usage, build-plan-status flag order, another project's mxcli test blocking exec.sh) were dropped because master already fixes them. Mendix 11.14.0 Beta, mxcli v0.25.0, toolkit c1efcb3 — AlvinnYeo - fix(build-plan-status): --refresh keeps an existing build-plan.json current, so a day of building no longer reads as 0 built — MendixMau diff --git a/agents/ba-agent.md b/agents/ba-agent.md index 6bf934d2..3febbc36 100644 --- a/agents/ba-agent.md +++ b/agents/ba-agent.md @@ -105,7 +105,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. - Run the full interview protocol from `conversion-runbook.md` §1 at every gate: homework first, 2-4 options with evidence, assumptions stated out loud, user answers in the terminal, decision written to the stage HTML *and* `PROJECT.md`, unknowns default + `ASSUMED` + proceed. - Never invent legacy intent from code alone — that's what Path C (the SME interview) is for. Log it as an open question if no SME is available, don't guess. - **Logging an open question is half the job. Raising it is the other half** — see the Open-questions batch below. A question written to a BRD and never put in the chat is not "logged", it is buried. -- **Read `interview-protocol.md` before raising anything.** Every question you hand over carries two named options and your recommendation; a bare question moves your analysis onto the user. Generate `docs/open-questions.html` with `bin/questions-report.sh` and give them the path. +- **Read `interview-protocol.md` before raising anything.** Every question you hand over carries two named options and your recommendation; a bare question moves your analysis onto the user. Generate `analysis/brd-report.html` with `bin/brd-report.sh` and give them the path. - `✋` gates (Stages 0 and 7) do not resolve to `ASSUMED` — they need an explicit `CONFIRMED` answer before the run proceeds. ## Workflow diff --git a/bin/brd-report.sh b/bin/brd-report.sh index 03e9040b..10edb651 100755 --- a/bin/brd-report.sh +++ b/bin/brd-report.sh @@ -40,7 +40,7 @@ # this instrument never judges whether the CONTENT is right, only whether it is there. # # Usage: -# brd-report.sh [--html PATH] [--json] [--quiet] +# brd-report.sh [--html PATH] [--stage N] [--json] [--quiet] # # exit 0 rendered (faults included — a fault is information, and the gate is gate-check's job) # 2 usage 3 no BRDs found (never a pass) 4 a BRD is not valid JSON @@ -60,12 +60,13 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat >&2 <<'USAGE' -usage: brd-report.sh [--html PATH] [--json] [--quiet] +usage: brd-report.sh [--html PATH] [--stage N] [--json] [--quiet] Renders the Stage 2 BRD surface from every BRD in the project, whatever produced them — a pipeline extractor, kb-generation.md, an SME interview, or a text editor. --html PATH where to write (default: /analysis/brd-report.html) + --stage N open questions up to stage N (the gate passes its own stage) --json machine-readable summary on stdout instead of prose --quiet render, say nothing @@ -80,11 +81,13 @@ shift 2>/dev/null || true case "$PROJECT_DIR" in -h|--help|'') usage ;; esac HTML_OUT="" +STAGE_ARG="" AS_JSON=0 QUIET=0 while [ $# -gt 0 ]; do case "$1" in --html) HTML_OUT="${2:-}"; shift 2 ;; + --stage) STAGE_ARG="${2:-}"; shift 2 ;; --json) AS_JSON=1; shift ;; --quiet) QUIET=1; shift ;; -h|--help) usage ;; @@ -101,7 +104,17 @@ require_py BRD_FILES=$(discover_brds "$PROJECT_DIR") BRD_COUNT=$(count_lines "$BRD_FILES") -if [ "$BRD_COUNT" = "0" ]; then +# Stage 0/1 has no BRDs yet, but the gate can still block on sme-questions.md questions and must +# hand the user a page. With no BRDs and no questions found, nothing is rendered (exit 3). +SME_ONLY=0 +if [ "$BRD_COUNT" = "0" ] && [ -x "$HERE/open-questions.sh" ]; then + _n=$(bash "$HERE/open-questions.sh" "$PROJECT_DIR" ${STAGE_ARG:+--stage "$STAGE_ARG"} --json 2>/dev/null | "$PY" -c 'import json,sys +try: print(json.load(sys.stdin)["counts"]["total"]) +except Exception: print(0)' 2>/dev/null) + [ "${_n:-0}" -gt 0 ] 2>/dev/null && SME_ONLY=1 +fi + +if [ "$BRD_COUNT" = "0" ] && [ "$SME_ONLY" = "0" ]; then # An empty result set is never a pass (report-schema.md). Distinct exit code so no caller # can read "found nothing" as "nothing to find". echo "brd-report: no BRDs found under $PROJECT_DIR — nothing rendered." >&2 @@ -114,7 +127,7 @@ fi mkdir -p "$(dirname "$HTML_OUT")" 2>/dev/null || true BRD_FILES="$BRD_FILES" BRD_COUNT="$BRD_COUNT" HTML_OUT="$HTML_OUT" AS_JSON="$AS_JSON" \ -QUIET="$QUIET" PROJECT_DIR="$PROJECT_DIR" "$PY" <<'PY' +QUIET="$QUIET" PROJECT_DIR="$PROJECT_DIR" TOOLKIT_BIN="$HERE" STAGE_ARG="$STAGE_ARG" "$PY" <<'PY' import json, os, sys, html, datetime files = [f for f in os.environ['BRD_FILES'].splitlines() if f.strip()] @@ -749,12 +762,8 @@ for a in analysed: note='

Rules found inside extracted logic that the domain model does ' 'not show. Derived from microflows[].hiddenRules.

'), section_block(a, 'openQuestions', 'Open questions', - '
    ' - + ''.join('
  • ' + esc(q.get('question') if isinstance(q, dict) else q) - + (f' {esc(q.get("status"))}' - if isinstance(q, dict) and q.get('status') else '') - + '
  • ' for q in a['raw']['openQuestions']) - + '
'), + '

Listed with their state, drafting position and process step in ' + 'Requirements and open questions at the top of this page.

'), ] chips = ''.join(f'{esc(m)}' for m in a['modules']) \ @@ -768,6 +777,331 @@ for a in analysed: f'

{esc(a["path"])}

' '') +# --- process map + open questions, fused (2026-10-16) ----------------------------------------- +# +# The page opens with the process map (pass 1: no screens needed) and lists, under it, every +# requirement (use case) and every open question or assumption. A click on a step filters that +# list to the rows about the step. Rows tied to no step stay listed under "whole app". +# +# The map is NOT drawn here: `project-bin/process-map.js build --embed` renders it (the one +# renderer, also used for the Stage 3 map beside the wireframes) and this page frames it. The +# open questions are NOT collected here either: `bin/open-questions.sh --json` is the one +# collector. This block only joins the two onto the BRDs. It replaces docs/open-questions.html. +import subprocess, shutil, tempfile, re + +toolkit_bin = os.environ['TOOLKIT_BIN'] +stage_arg = os.environ.get('STAGE_ARG', '') +proj_abs = os.path.abspath(project) + +def _find_model(): + cands = [os.path.join(proj_abs, 'design', 'process-model.json')] + adir = os.path.join(proj_abs, 'analysis') + if os.path.isdir(adir): + for d in sorted(os.listdir(adir)): + cands.append(os.path.join(adir, d, 'design', 'process-model.json')) + for c in cands: + if os.path.isfile(c): + return c + return None + +def _find_pm(): + for c in (os.path.join(toolkit_bin, '..', 'project-bin', 'process-map.js'), + os.path.join(proj_abs, 'bin', 'process-map.js')): + if os.path.isfile(c): + return os.path.normpath(c) + return None + +pm_data, pm_frame, pm_note = None, '', '' +_model, _pm, _node = _find_model(), _find_pm(), shutil.which('node') +if not _model: + pm_note = ('No process map yet. Draw it at Stage 2 close (skills/process-map.md, pass 1); ' + 'until then every requirement and question is listed under "whole app".') +elif not (_pm and _node): + pm_note = ('A process model exists (' + os.path.relpath(_model, proj_abs) + ') but the map ' + 'could not be drawn: ' + ('node is not on PATH' if not _node else 'process-map.js not found') + '.') +else: + _tmp = tempfile.mkdtemp(prefix='brdmap.') + try: + _r = subprocess.run([_node, _pm, 'build', '--embed', '--project', proj_abs, '--model', _model, + '--out', os.path.join(_tmp, 'embed.html'), + '--data-out', os.path.join(_tmp, 'data.json')], + capture_output=True, text=True, timeout=120) + if _r.returncode == 0 and os.path.isfile(os.path.join(_tmp, 'data.json')): + with open(os.path.join(_tmp, 'data.json')) as fh: + pm_data = json.load(fh) + with open(os.path.join(_tmp, 'embed.html')) as fh: + pm_frame = fh.read() + else: + _errs = [l for l in (_r.stdout + _r.stderr).splitlines() if l.startswith(('ERROR', 'process-map:'))] + pm_note = ('The process model (' + os.path.relpath(_model, proj_abs) + ') did not build: ' + + ' | '.join(_errs[:4]) + '. Fix the model (skills/process-map.md) and re-run.') + except Exception as e: + pm_note = 'The process map could not be drawn: ' + str(e) + finally: + shutil.rmtree(_tmp, ignore_errors=True) + +# open questions: the one collector +oq, oq_note = None, '' +try: + _c = subprocess.run(['bash', os.path.join(toolkit_bin, 'open-questions.sh'), project] + + (['--stage', stage_arg] if stage_arg else []) + ['--json'], + capture_output=True, text=True, timeout=120) + oq = json.loads(_c.stdout) +except Exception as e: + oq_note = ('The open-questions collector did not run (' + str(e)[:120] + '), so states are unknown. ' + 'The questions below are read straight from the BRDs.') + oq = None + +brd_of_base = {os.path.basename(a['path']): a['id'] for a in analysed} +BIG = 1 << 30 +step_ord, step_meta = {}, {} +if pm_data: + for _p in pm_data['processes']: + for _s in _p['steps']: + step_ord[_s['id']] = len(step_ord) + step_meta[_s['id']] = {'label': f"{_p['id']} step {_s['n']}: {_s['title']}", 'proc': _p['id'], 'n': _s['n']} +steps_of_ref = {} # "F001:Q01" -> {step ids}, from the map's items +if pm_data: + for _it in pm_data['items']: + for _ref in _it['refs']: + steps_of_ref.setdefault(_ref, set()).update(_it['steps']) + +def _order(steps): + return sorted(steps, key=lambda s: step_ord.get(s, BIG)) + +def _first(steps): + return min([step_ord.get(s, BIG) for s in steps] or [BIG]) + +def step_chips(steps): + if not steps: + return 'whole app' + return ''.join(f'{esc(step_meta[s]["proc"])} · {step_meta[s]["n"]}' + for s in _order(steps)) + +def _attr_steps(steps): + return esc(' '.join(_order(steps))) + +# rows: (..., steps, html). The second-last element is always the step list. +req_rows, q_rows = [], [] +for a in analysed: + for u in a['raw']['useCases']: + if not isinstance(u, dict) or not u.get('id'): + continue + ref = f"{a['id']}:{u['id']}" + info = (pm_data or {}).get('useCases', {}).get(ref) + steps = info['steps'] if info else [] + actors, _ = alist(u, 'actors', 'actor') + why = '' + if info and info.get('standalone'): + why = f'
Not part of a process: {esc(info["standalone"])}
' + actor_html = f'
{esc("; ".join(str(x) for x in actors))}
' if actors else '' + req_rows.append(('req', 'req', (_first(steps), ref), steps, + f'
' + f'
{esc(ref)} {esc(u.get("title"))}
' + + actor_html + why + f'
{step_chips(steps)} BRD detail
')) + +STATE_NOTE = { + 'UNRAISED': 'Never put to you. These block the gate.', + 'UNRECOGNISED': 'Status text no rule could map. Treated as unraised.', + 'RAISED': 'Put to you, awaiting your reply.', + 'ASSUMED': 'A position was taken and you consented to it. Recorded, not blocking.', + 'ANSWERED': 'Settled with a recorded answer.', + 'MOOT': 'Descoped, not silently dropped.', +} +STATE_ORDER = ['UNRECOGNISED', 'UNRAISED', 'RAISED', 'ASSUMED', 'ANSWERED', 'MOOT'] +BLOCKING = ('UNRAISED', 'UNRECOGNISED') + +def q_row(state, qid, origin, text, prop, steps, meta, rid): + cls = 'block' if state in BLOCKING else 'ok' + if prop: + prophtml = ('
Drafting position: confirm or overturn' + f'
{esc(prop)}
') + elif state in BLOCKING: + prophtml = ('
No proposed answer' + '
Handed over bare. The stage that raised it should propose two named options ' + 'and a recommendation before it goes to the user.
') + else: + prophtml = '' + meta_html = f'
{meta}
' if meta else '' + return ('q', state, (_first(steps), str(qid)), steps, + f'
' + f'
{esc(qid)} {esc(state)} ' + f'{esc(origin)}
{esc(text)}
{prophtml}{meta_html}' + f'
{step_chips(steps)}
') + +if oq is not None: + for q in oq.get('questions', []): + base = os.path.basename(str(q.get('origin', ''))) + bid = brd_of_base.get(base) + steps = sorted(steps_of_ref.get(f"{bid}:{q.get('id')}", [])) if bid else [] + meta = ' · '.join(x for x in [ + (f'recorded as {esc(str(q.get("rawStatus"))[:120])}' if q.get('rawStatus') else ''), + esc(q.get('reason') or ''), (f'stage {esc(q.get("stage"))}' if q.get('stage') is not None else '')] if x) + rid = f"q-{bid}-{q.get('id')}" if bid else 'q-' + re.sub('[^A-Za-z0-9_-]', '-', base) + '-' + str(q.get('id')) + q_rows.append(q_row(q.get('state', 'UNRECOGNISED'), q.get('id', '?'), q.get('origin', ''), q.get('question', ''), + (q.get('assumption') or '').strip(), steps, meta, rid)) +else: + for a in analysed: + for q in a['raw']['openQuestions']: + if not isinstance(q, dict): + continue + steps = sorted(steps_of_ref.get(f"{a['id']}:{q.get('id')}", [])) + q_rows.append(q_row('NOT COLLECTED', q.get('id', '?'), os.path.basename(a['path']), q.get('question', ''), + (q.get('assumption') or '').strip(), steps, '', f"q-{a['id']}-{q.get('id')}")) +for it in (pm_data or {}).get('items', []): + if it['kind'] != 'decision': + continue + steps = it['steps'] + q_rows.append(('q', 'ASSUMED', (_first(steps), it['id']), steps, + f'
' + f'
{esc(it["id"])} ASSUMPTION ' + f'decision register
{esc(it["text"])}
' + f'
{step_chips(steps)}
')) + +def _srank(r): + return STATE_ORDER.index(r[1]) if r[1] in STATE_ORDER else 9 + +def _pick(rows, whole): + sel = [r for r in rows if (not r[-2]) == whole] + if rows is q_rows: + sel.sort(key=(lambda r: (_srank(r), r[2])) if whole else (lambda r: (r[2], _srank(r)))) + else: + sel.sort(key=lambda r: r[2]) + return sel + +def _group(title, rows): + if not rows: + return '' + return (f'

{esc(title)} {len(rows)}

' + + ''.join(r[-1] for r in rows) + '
') + +n_blocking = (oq or {}).get('blocking', 0) +if oq is None: + q_banner = f'' +else: + _bare = [q for q in oq.get('questions', []) if q.get('state') in BLOCKING and not (q.get('assumption') or '').strip()] + q_banner = ((f'') + if n_blocking else '') + if _bare: + q_banner += (f'') + _cnt = oq.get('counts', {}) + _rows_t = ''.join(f'{s}{_cnt.get(s, 0)}' + f'{esc(STATE_NOTE[s])}' for s in STATE_ORDER if _cnt.get(s, 0)) + if _rows_t: + q_banner += ('
What was found, by state
' + '' + _rows_t + '
StateCountMeaning
') + +n_by_step = len([r for r in req_rows + q_rows if r[-2]]) +n_whole = len(req_rows) + len(q_rows) - n_by_step +by_step_html = '' +if n_by_step: + by_step_html = (f'

About the steps of the map ({n_by_step})

' + + _group('Requirements', _pick(req_rows, False)) + _group('Open questions and assumptions', _pick(q_rows, False))) +whole_html = _group('Requirements', _pick(req_rows, True)) + _group('Open questions and assumptions', _pick(q_rows, True)) + +# First paint height: the frame reports its real height once it has a width, but a screenshot or a slow +# frame must not show a clipped map in the meantime. About 105 px per lane row, plus legend and title. +pm_h0 = 70 + sum(120 + 105 * len({st.get('lane') for st in pr.get('steps', [])}) for pr in (pm_data or {}).get('processes', [])) if pm_frame else 120 +if pm_frame: + map_html = ('

Process map

' + '

One map per business process. Rows are the people and the system, cards are the steps in order. ' + 'Click a step to see the requirements and open questions about it below.

' + f'
') +else: + map_html = f'

Process map

' + +list_html = ('
' + '

Requirements and open questions

' + q_banner + by_step_html + + f'

Whole app ({n_whole})

' + '

Not tied to one step: they apply to the whole app or to one kind of data, or no map is drawn yet.

' + + (f'
Show the {n_whole} whole-app items{whole_html}
' if n_whole > 8 else whole_html) + + '
') + +PM_JS = """ +(function(){ +var META = %s; +var list = document.getElementById('pm-list'); if (!list) return; +var frame = document.getElementById('pm-frame'), bar = document.getElementById('pm-bar'), + txt = document.getElementById('pm-bar-text'), whole = document.getElementById('pm-whole'); +var current = ''; +function post(){ if (frame && frame.contentWindow) frame.contentWindow.postMessage({pm:'select', step:current}, '*'); } +function apply(id){ + current = id || ''; + var rows = list.querySelectorAll('.pmrow'), n = {req:0, q:0}; + Array.prototype.forEach.call(rows, function(r){ + var on = !current || (' ' + r.getAttribute('data-steps') + ' ').indexOf(' ' + current + ' ') >= 0; + r.hidden = !on; if (on && current) n[r.getAttribute('data-kind') === 'req' ? 'req' : 'q']++; + }); + Array.prototype.forEach.call(list.querySelectorAll('.pmgroup'), function(g){ + var v = g.querySelectorAll('.pmrow:not([hidden])').length; g.hidden = !!current && !v; + var c = g.querySelector('.cnt'); if (c) c.textContent = String(v); }); + Array.prototype.forEach.call(list.querySelectorAll('.pmh'), function(e){ e.hidden = !!current; }); + if (whole) whole.hidden = !!current; + if (bar){ bar.hidden = !current; if (current && META[current]) txt.textContent = META[current] + ': ' + n.req + ' requirement(s), ' + n.q + ' question(s) or assumption(s)'; } + post(); +} +function pick(id, scroll){ + apply(id); + try{ history.replaceState(null, '', id ? '#step=' + id : location.pathname + location.search); }catch(e){} + if (scroll) { var t = document.getElementById('pm-title'); if (t) t.scrollIntoView({behavior:'smooth', block:'start'}); } +} +window.addEventListener('message', function(e){ + if (!frame || e.source !== frame.contentWindow) return; + var d = e.data || {}; + if (d.pm === 'height') { if (d.h > 80) frame.style.height = d.h + 'px'; } + else if (d.pm === 'step') pick(d.step, true); + else if (d.pm === 'ready') post(); +}); +list.addEventListener('click', function(e){ + var a = e.target.closest ? e.target.closest('a.stepchip') : null; + if (a){ e.preventDefault(); pick(a.getAttribute('data-step'), false); var m = document.getElementById('pm-map'); if (m) m.scrollIntoView({behavior:'smooth', block:'start'}); } +}); +document.getElementById('pm-clear').addEventListener('click', function(){ pick('', false); }); +var m = /#step=([^&]+)/.exec(location.hash); if (m && META[decodeURIComponent(m[1])]) apply(decodeURIComponent(m[1])); +})(); +""" % json.dumps({k: v['label'] for k, v in step_meta.items()}) + +PM_CSS = """ + .pmmap iframe { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-height: 120px; display: block; } + .banner { border-radius: var(--radius); padding: 11px 15px; margin: 12px 0; font-size: 14px; } + .banner.pass { background: var(--pass-bg); color: var(--pass-ink); } + .banner.fail { background: var(--fail-bg); color: var(--fail-ink); } + .banner.warn { background: var(--warn-bg); color: var(--warn-ink); } + .pmh { font-size: 15px; margin: 26px 0 4px; } + .pmgroup h4 { margin: 18px 0 8px; } + .pmrow { background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--ink-faint); + border-radius: var(--radius); padding: 11px 14px; margin: 8px 0; overflow-wrap: anywhere; } + .pmrow[hidden], .pmgroup[hidden], .pmh[hidden], #pm-whole[hidden], .pmbar[hidden] { display: none; } + .pmrow.q.block { border-left-color: #dc2626; } + .pmrow.req { border-left-color: var(--accent); } + .pmhead { font-size: 14px; } + .pmsub, .pmrow .src { font-size: 12.5px; color: var(--ink-soft); margin-top: 3px; } + .qtext { font-size: 14.5px; margin-top: 4px; } + .prop, .gap { margin-top: 8px; padding: 8px 11px; border-radius: 8px; font-size: 13.5px; } + .prop { background: var(--warn-bg); color: var(--warn-ink); } + .gap { background: #f1f5f9; color: var(--ink-soft); } + .lbl { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; font-weight: 700; margin-bottom: 2px; } + .st { font-size: 10.5px; font-weight: 700; letter-spacing: .05em; padding: 1px 8px; border-radius: 999px; } + .st-block { background: var(--fail-bg); color: var(--fail-ink); } + .st-ok { background: #eef2f7; color: var(--ink-soft); } + .pmchips { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } + .stepchip { font-size: 11.5px; text-decoration: none; color: var(--accent); background: #eef4fb; border: 1px solid #cfe0f3; + border-radius: 999px; padding: 2px 10px; } + .stepchip:hover { background: #dde9f7; } + .whole { font-size: 11.5px; color: var(--ink-faint); border: 1px dashed var(--line); border-radius: 999px; padding: 2px 10px; } + .pmdetail { font-size: 11.5px; color: var(--ink-faint); margin-left: auto; } + .pmbar { position: sticky; top: 0; z-index: 5; display: flex; gap: 12px; align-items: center; justify-content: space-between; + background: var(--accent); color: #fff; border-radius: var(--radius); padding: 9px 14px; margin: 0 0 10px; font-size: 14px; } + .pmbar button { font: inherit; color: var(--accent); background: #fff; border: 0; border-radius: 6px; padding: 4px 12px; cursor: pointer; flex: none; } + details.more { margin: 8px 0; font-size: 13.5px; } + details.more summary { cursor: pointer; color: var(--ink-soft); } + @media (max-width: 560px) { body { padding: 20px 16px 60px; } .brd { padding: 16px 14px; } h1 { font-size: 23px; } } +""" + stat = lambda val, lbl: (f'
{esc(val)}
' f'
{esc(lbl)}
') @@ -882,15 +1216,17 @@ page = f""" .risk-medium {{ color: var(--warn-ink); font-weight: 700; }} .footer {{ margin-top: 56px; font-size: 12px; color: var(--ink-faint); border-top: 1px solid var(--line); padding-top: 14px; }} - +{PM_CSS}
-

Stage 2 — what the BRDs actually say

-

Rendered {esc(datetime.datetime.now().strftime('%Y-%m-%d'))} from - {len(analysed)} BRD(s) in {len(kb_dirs)} knowledge-base director(ies) — whatever produced - them. Every section carries a verdict, so a section that is empty because this kind of BRD - has nothing to put there reads differently from one that is empty because something broke.

+

{'Open questions, stage ' + stage_arg + ' view' if (not analysed and stage_arg) else ('Open questions' if not analysed else 'Stage 2 — what the BRDs actually say')}

+

{('Rendered ' + esc(datetime.datetime.now().strftime('%Y-%m-%d')) + '. No BRDs exist yet: these are the questions collected from analysis/sme-questions.md.') if not analysed else ('Rendered ' + esc(datetime.datetime.now().strftime('%Y-%m-%d')) + ' from ' + str(len(analysed)) + ' BRD(s) in ' + str(len(kb_dirs)) + ' knowledge-base director(ies), whatever produced them. Every section carries a verdict, so a section that is empty because this kind of BRD has nothing to put there reads differently from one that is empty because something broke.')} + {esc('Questions shown: up to stage ' + stage_arg + '.') if stage_arg else ''}

+ + {map_html} + + {list_html}
{stat(len(analysed), 'BRDs')} @@ -940,6 +1276,7 @@ page = f""" directory, not the first one.
+ """ diff --git a/bin/gate-check.sh b/bin/gate-check.sh index accd8dc6..680c911c 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -3318,14 +3318,15 @@ if [ -n "$REQUESTED_STAGE" ]; then echo "" >&2 echo "Gate BLOCKED: $blk question(s) at stage $st have never been raised with the user." >&2 echo " $counts" >&2 - # Write the triage report the user reads, alongside the batch the agent reads. - # A terminal dump scrolls away and only the agent ever sees it; the HTML is the artefact - # a human can open, sort by state, and hand to someone else. --no-html suppresses it for - # the same reason it suppresses the board: a read-only query must not dirty a tree. - QR_SCRIPT="$TOOLKIT_DIR/bin/questions-report.sh" - QR_OUT="$PROJECT_DIR/docs/open-questions.html" + # Write the report the user reads, alongside the batch the agent reads. A terminal dump + # scrolls away and only the agent ever sees it; the HTML is the artefact a human can open and + # hand to someone else. Since 2026-10-16 the questions live in the Stage 2 BRD report, under + # the process map (bin/brd-report.sh), not in docs/open-questions.html. --no-html suppresses + # it for the same reason it suppresses the board: a read-only query must not dirty a tree. + QR_SCRIPT="$TOOLKIT_DIR/bin/brd-report.sh" + QR_OUT="$PROJECT_DIR/analysis/brd-report.html" if [ "$HTML_MODE" != "never" ] && [ -x "$QR_SCRIPT" ]; then - "$QR_SCRIPT" "$PROJECT_DIR" $arg -o "$QR_OUT" --quiet >/dev/null 2>&1 + "$QR_SCRIPT" "$PROJECT_DIR" $arg --html "$QR_OUT" --quiet >/dev/null 2>&1 [ -f "$QR_OUT" ] || QR_OUT="" else QR_OUT="" @@ -3334,7 +3335,9 @@ if [ -n "$REQUESTED_STAGE" ]; then echo "" >&2 echo " See them, as a chat-ready batch:" >&2 echo " $OQ_SCRIPT $PROJECT_DIR${arg:+ $arg}" >&2 - [ -n "$QR_OUT" ] && echo " Triage report (open this, and give the user the path): $QR_OUT" >&2 + [ -n "$QR_OUT" ] && echo " BRD report, questions under the process map (open this, and give the user the path): $QR_OUT" >&2 + # Back-compat: the retired page is never read and never fails a gate; it only gets pointed away from. + [ -f "$PROJECT_DIR/docs/open-questions.html" ] && echo " (docs/open-questions.html is the retired page: its content is in analysis/brd-report.html now. Safe to delete.)" >&2 echo "" >&2 echo " Raising is mandatory; assuming is not forbidden. Anything the user hands back" >&2 echo " ('you decide') is recorded ASSUMED with consentBy/consentAt and stops blocking." >&2 diff --git a/bin/init-project.sh b/bin/init-project.sh index cabed48e..69cd4c81 100755 --- a/bin/init-project.sh +++ b/bin/init-project.sh @@ -472,7 +472,7 @@ fi # analysis/ — THE OUTPUT SIDE, and it was never scaffolded (fixed 2026-08-20). # # Every rendered surface in the pipeline lands here: triage.html (bin/triage-report.sh), -# source-sufficiency.html, brd-report.html (bin/brd-report.sh), open-questions.html. gate-check.sh +# source-sufficiency.html, brd-report.html (bin/brd-report.sh; the open questions live in it). gate-check.sh # resolves $ANALYSIS_BASE against it. sources/ was created here from day one; analysis/ was not, # on the unstated assumption that an extraction pipeline would `mkdir -p` it on its way past. # @@ -504,8 +504,7 @@ and the honest way to change one is to re-run its producer. | `source-sufficiency.html` | `bin/source-sufficiency.sh report` | 0 | | `triage.html` | `bin/triage-report.sh ` | 0 | | `extraction-report.html` | the extraction pipeline, or `kb-generation.md` for a document corpus | 1 | -| `brd-report.html` | `bin/brd-report.sh ` | 2 | -| `open-questions.html` | `bin/questions-report.sh --stage N` | any gate | +| `brd-report.html` | `bin/brd-report.sh [--stage N]`: process map, requirements and open questions | 2, any gate | **This folder exists in every entry mode.** A requirements-driven or greenfield project runs no extraction pipeline, but it still produces a sufficiency grade, a scope decision and a BRD report diff --git a/bin/install-claude-hooks.sh b/bin/install-claude-hooks.sh index 1dff2e00..758c7eac 100755 --- a/bin/install-claude-hooks.sh +++ b/bin/install-claude-hooks.sh @@ -68,7 +68,7 @@ TIER 2 --full Changes your working rhythm. Useful after a bad bill; precompact-guard Warns once before a compaction discards unfiled work. interview-gate BLOCKS the end of a turn while questions the pipeline generated have never been put to you, and writes - docs/open-questions.html for you to review. Only fires on + analysis/brd-report.html for you to review. Only fires on projects that actually have BRDs; never blocks the same question set twice, so it cannot trap a session. diff --git a/bin/lib/artifact-manifest.tsv b/bin/lib/artifact-manifest.tsv index e866a5eb..4a54214e 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -81,7 +81,7 @@ extraction-report 1 analysis/*/knowledge-base/extraction-report.html|analysis/kn source-ledger 1 analysis/source-ledger.html bin/source-ledger.sh report — every inventoried source file and what consumed it (Stage 1 owes a disposition per row; gate-check blocks 1–2 on the check) bin/gate-check.sh (LEDGER block — the check; the html is the human-facing table of the same rows) migration,requirements,existing-app-change report brds 2 analysis/*/knowledge-base/brd/*.brd.json|analysis/knowledge-base/brd/*.brd.json|knowledge-base/brd/*.brd.json pipeline generators (migration), skills/kb-generation.md (documents), skills/brd-generation.md (by hand) — the reader must not care which bin/lib/discover-brds.sh:43 (the one path authority), bin/gate-check.sh:773 (check_stage_2), bin/brd-report.sh, bin/open-questions.sh migration,requirements,existing-app-change gate brd-validation 2 analysis/*/knowledge-base/reports/validation-report.md|analysis/knowledge-base/reports/validation-report.md|knowledge-base/reports/validation-report.md skills/brd-validation.md loop, run to Clean bin/gate-check.sh:777-807 (check_stage_2 Stop-condition test) migration,requirements,existing-app-change gate -brd-surface 2 analysis/brd-report.html bin/brd-report.sh (default output, bin/brd-report.sh:113) conversion-runbook.md §2 Stage-2 Surface row, parsed at bin/gate-check.sh:1284 (stage_surface_cells) migration,requirements,existing-app-change report +brd-surface 2 analysis/brd-report.html bin/brd-report.sh (default output, bin/brd-report.sh:113); also holds the pass-1 process map and the open questions (replaced docs/open-questions.html, 2026-10-16) conversion-runbook.md §2 Stage-2 Surface row, parsed at bin/gate-check.sh:1284 (stage_surface_cells) migration,requirements,existing-app-change report module-design 3 architecture/module-design.html|module-design.html skills/modularize-domain.md:88 (the human-facing sign-off artifact) skills/modularize-domain.md:109 (gate checklist), conversion-runbook.md:478 Stage-3 Surface row (parsed at bin/gate-check.sh:1284) migration,requirements,existing-app-change report fit-gap 3 architecture/fit-gap.md skills/architecture-blueprint.md Step 4 bin/gate-check.sh:912 (check_stage_3), bin/gate-check.sh:939 (blueprint.html staleness input) migration,requirements,existing-app-change gate blueprint-md 3 architecture/blueprint.md skills/architecture-blueprint.md Steps 2-6 — the markdown is the source of truth bin/gate-check.sh:939 (staleness input), skills/architecture-blueprint.md:54 (render source for blueprint.html) migration,requirements,existing-app-change report diff --git a/bin/questions-report.sh b/bin/questions-report.sh deleted file mode 100755 index 15cee9c2..00000000 --- a/bin/questions-report.sh +++ /dev/null @@ -1,299 +0,0 @@ -#!/bin/bash -# questions-report.sh — render a project's open questions as a reviewable HTML page. -# -# WHY THIS EXISTS -# --------------- -# bin/open-questions.sh already produces a chat-ready batch. That solves "the agent has the -# questions". It does not solve the two things the user actually reported missing: -# -# 1. There is no surface showing WHAT WAS FOUND — what got examined, what is still open, -# and which open items are blocking — that a human can read away from the terminal. -# 2. Most questions arrive with no proposed answer. A question with no proposed answer is -# work handed back to the user. A question with two named options and a recommendation -# is a decision they can make in five seconds. -# -# This script renders (1), and marks every question that lacks (2) as a defect in the -# generating stage rather than quietly presenting a bare question. -# -# Usage: -# bin/questions-report.sh [--stage N] [-o ] [--quiet] -# -# Default output: /docs/open-questions.html -# -# Exit codes: 0 = report written, nothing blocking. 1 = report written, blocking questions -# exist. 2 = usage/environment error. 3 = nothing was examined (NOT a clean -# result — same meaning as open-questions.sh). - -set -uo pipefail - -# Portability: resolve a real Python 3 by EXECUTING candidates, not by looking one up on PATH. -# On Windows `python3` is usually the Microsoft Store alias stub, which `command -v` finds and -# which then opens the Store instead of running. See bin/lib/portable.sh. -# shellcheck disable=SC1091 -. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/portable.sh" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COLLECTOR="$SCRIPT_DIR/open-questions.sh" - -PROJECT_DIR="" -STAGE_ARGS=() -OUT="" -QUIET=0 - -while [ $# -gt 0 ]; do - case "$1" in - --stage) STAGE_ARGS+=(--stage "${2:-}"); shift 2 ;; - -o|--output) OUT="${2:-}"; shift 2 ;; - --quiet) QUIET=1; shift ;; - -h|--help) sed -n '2,28p' "${BASH_SOURCE[0]}"; exit 0 ;; - -*) echo "questions-report.sh: unknown option $1" >&2; exit 2 ;; - *) if [ -z "$PROJECT_DIR" ]; then PROJECT_DIR="$1"; else STAGE_ARGS+=("$1"); fi; shift ;; - esac -done - -[ -n "$PROJECT_DIR" ] || { echo "usage: questions-report.sh [--stage N] [-o out.html]" >&2; exit 2; } -[ -d "$PROJECT_DIR" ] || { echo "questions-report.sh: not a directory: $PROJECT_DIR" >&2; exit 2; } -[ -x "$COLLECTOR" ] || [ -f "$COLLECTOR" ] || { echo "questions-report.sh: collector not found: $COLLECTOR" >&2; exit 2; } - -PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)" -[ -n "$OUT" ] || OUT="$PROJECT_DIR/docs/open-questions.html" - -JSON=$(bash "$COLLECTOR" "$PROJECT_DIR" "${STAGE_ARGS[@]+"${STAGE_ARGS[@]}"}" --json 2>/dev/null) -COLLECT_RC=$? - -# rc 3 means no BRDs and no sme-questions.md were found. That is not "no open questions"; -# it means the instrument found nothing to read, and reporting it as clean is the exact -# class of false-green this toolkit keeps getting bitten by. -# Belt and braces: the flag is read from the payload as well as the exit code. rc=3 was -# unreachable in --json mode until 2026-08-12, and this script trusting it alone is how that -# went unnoticed — it rendered a clean-looking report over an empty directory. -NOTHING=$(printf '%s' "$JSON" | grep -c '"nothingExamined": *true' | head -1) -if [ "$COLLECT_RC" -eq 3 ] || [ "${NOTHING:-0}" -gt 0 ] || [ -z "$JSON" ]; then - echo "questions-report.sh: nothing was examined (no BRDs, no sme-questions.md) — refusing to" >&2 - echo " write a report that would read as 'no open questions'. Exit 3." >&2 - exit 3 -fi - -mkdir -p "$(dirname "$OUT")" || { echo "questions-report.sh: cannot create $(dirname "$OUT")" >&2; exit 2; } - -require_py - -PROJECT_NAME="$(basename "$PROJECT_DIR")" -GENERATED="$(date '+%Y-%m-%d %H:%M')" - -# The collector's JSON goes through a temp FILE, not a pipe. `"$PY" - <<'PYEOF'` takes its -# program from stdin, so a pipe into the same command is swallowed by the heredoc and json.load -# reads an empty string. Costing a temp file is cheaper than an inline-quoted program. -JSON_TMP=$(mktemp "${TMPDIR:-/tmp}/qreport.XXXXXX.json") || exit 2 -trap 'rm -f "$JSON_TMP"' EXIT -printf '%s' "$JSON" > "$JSON_TMP" - -PROJECT_NAME="$PROJECT_NAME" GENERATED="$GENERATED" JSON_TMP="$JSON_TMP" \ - PROJECT_DIR="$PROJECT_DIR" OUT="$OUT" "$PY" - <<'PYEOF' -import html, json, os, sys - -with open(os.environ["JSON_TMP"], encoding="utf-8") as fh: - d = json.load(fh) -qs = d.get("questions", []) -counts = d.get("counts", {}) -ex = d.get("examined", {}) -blocking = d.get("blocking", 0) -project = os.environ["PROJECT_NAME"] -generated = os.environ["GENERATED"] -out_path = os.environ["OUT"] - -BLOCKING_STATES = {"UNRAISED", "UNRECOGNISED"} - -# Order: what needs the user first, then what is merely on record. -ORDER = ["UNRECOGNISED", "UNRAISED", "RAISED", "ASSUMED", "ANSWERED", "MOOT"] -STATE_NOTE = { - "UNRAISED": "Never put to you. These block the gate.", - "UNRECOGNISED": "Status text no rule could map. Treated as unraised — a status nobody can parse is not an answer.", - "RAISED": "Put to you, awaiting your reply.", - "ASSUMED": "A position was taken AND you consented to it. Recorded, not blocking.", - "ANSWERED": "Settled with a recorded answer.", - "MOOT": "Descoped — genuinely dead, not silently dropped.", -} - -def esc(s): - return html.escape(str(s or "")) - -# A question the user can act on needs a proposed answer. One without is not "a question", -# it is unfinished work by the stage that produced it — so it gets called out as such. -def has_proposal(q): - return bool((q.get("assumption") or "").strip()) - -groups = {} -for q in qs: - groups.setdefault(q.get("state", "UNRECOGNISED"), []).append(q) - -actionable = [q for q in qs if q.get("state") in BLOCKING_STATES] -no_proposal = [q for q in actionable if not has_proposal(q)] - -rows = [] -for st in ORDER: - n = counts.get(st, 0) - if n: - cls = "b" if st in BLOCKING_STATES else "" - rows.append(f'{st}{n}{esc(STATE_NOTE.get(st,""))}') -tbl = "\n".join(rows) - -stage_filter = ex.get("stageFilter") -scope = f"stage ≤ {esc(stage_filter)}" if stage_filter else "all stages" - -cards = [] -for st in ORDER: - items = groups.get(st, []) - if not items: - continue - badge = "block" if st in BLOCKING_STATES else "ok" - cards.append(f'

{esc(st)} {len(items)}

') - cards.append(f'

{esc(STATE_NOTE.get(st,""))}

') - for q in items: - prop = (q.get("assumption") or "").strip() - raw = (q.get("rawStatus") or "").strip() - reason = (q.get("reason") or "").strip() - bits = [f'
'] - bits.append(f'
{esc(q.get("id","?"))} {esc(q.get("origin",""))}
') - bits.append(f'
{esc(q.get("question",""))}
') - if prop: - bits.append( - '
Drafting position — confirm or overturn' - f'
{esc(prop)}
') - elif st in BLOCKING_STATES: - bits.append( - '
No proposed answer' - '
This question was handed over bare. Before putting it to the user, the owning ' - 'stage should propose at least two named options and a recommendation — otherwise the ' - 'user is being asked to do the analysis the stage was meant to do.
') - meta = [] - if raw: - meta.append(f'recorded as: {esc(raw[:160])}') - if reason: - meta.append(esc(reason)) - if q.get("stage") is not None: - meta.append(f'stage {esc(q.get("stage"))}') - if meta: - bits.append('
' + " · ".join(meta) + "
") - bits.append("
") - cards.append("".join(bits)) - -banner_cls = "fail" if blocking else "pass" -banner_txt = (f"{blocking} question(s) have never been put to the user — this gate is blocked." - if blocking else - "No unraised questions. Every question on record has been seen by the user.") - -gapline = "" -if no_proposal: - gapline = (f'') - -doc = f""" - - -Open questions — {esc(project)} - -
-

Open questions — {esc(project)}

-

Generated {esc(generated)} · scope: {scope} · - examined {esc(ex.get("brdFiles",0))} BRD file(s) and {esc(ex.get("smeQuestionFiles",0))} SME question file(s), - {esc(ex.get("duplicatesCollapsed",0))} duplicate(s) collapsed

- - - {gapline} - -

What was found

- - {tbl} -
StateCountMeaning
- - {"".join(cards)} - - -
-""" - -with open(out_path, "w") as f: - f.write(doc) -PYEOF - -PY_RC=$? -[ "$PY_RC" -eq 0 ] || { echo "questions-report.sh: render failed (python exit $PY_RC)" >&2; exit 2; } - -if [ "$QUIET" -eq 0 ]; then - echo "Wrote: $OUT" - # Reads the temp file, not a pipe, and no 2>/dev/null: a summary that silently prints - # nothing when it breaks is worse than one that says why. - JSON_TMP="$JSON_TMP" "$PY" - <<'PYSUM' -import json, os -with open(os.environ["JSON_TMP"], encoding="utf-8") as fh: - d = json.load(fh) -c = d.get("counts", {}) -qs = d.get("questions", []) -bare = [q for q in qs - if q.get("state") in ("UNRAISED", "UNRECOGNISED") - and not (q.get("assumption") or "").strip()] -print(" blocking: %s total: %s without a proposed answer: %s" - % (d.get("blocking", 0), c.get("total", 0), len(bare))) -PYSUM -fi - -[ "$COLLECT_RC" -eq 1 ] && exit 1 -exit 0 diff --git a/claude-hooks/hooks/interview-gate.sh b/claude-hooks/hooks/interview-gate.sh index bf7a3f1f..b36c88df 100755 --- a/claude-hooks/hooks/interview-gate.sh +++ b/claude-hooks/hooks/interview-gate.sh @@ -50,7 +50,7 @@ except Exception: print("True") PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" TOOLKIT="__TOOLKIT_ROOT__" OQ="$TOOLKIT/bin/open-questions.sh" -QR="$TOOLKIT/bin/questions-report.sh" +QR="$TOOLKIT/bin/brd-report.sh" IM="$TOOLKIT/bin/interview-mode.sh" [ -x "$OQ" ] || exit 0 @@ -92,8 +92,8 @@ printf '%s' "$FP" > "$STATE" 2>/dev/null || true REPORT="" if [ -x "$QR" ]; then - "$QR" "$PROJECT_DIR" -o "$PROJECT_DIR/docs/open-questions.html" --quiet >/dev/null 2>&1 - [ -f "$PROJECT_DIR/docs/open-questions.html" ] && REPORT="$PROJECT_DIR/docs/open-questions.html" + "$QR" "$PROJECT_DIR" --html "$PROJECT_DIR/analysis/brd-report.html" --quiet >/dev/null 2>&1 + [ -f "$PROJECT_DIR/analysis/brd-report.html" ] && REPORT="$PROJECT_DIR/analysis/brd-report.html" fi # In auto mode — and when the user is remote — the report is the whole job. Refreshing it @@ -113,7 +113,7 @@ REASON="$BLOCKING question(s) in this project have never been put to the user, a Before you stop: put them in the chat. Read $TOOLKIT/skills/interview-protocol.md — every question carries two named options and your recommendation; a bare question hands your analysis back to the user. Batch them, then end the turn and wait for answers. " [ -n "$REPORT" ] && REASON="${REASON} -Triage report written for the user to review: $REPORT — give them this path. +BRD report (open questions under the process map) written for the user to review: $REPORT — give them this path. " REASON="${REASON} This project is in interview mode '$MODE'. If the user wants speed over involvement here, diff --git a/docs/pipeline-walks.html b/docs/pipeline-walks.html index dbd31540..ad08b7bb 100644 --- a/docs/pipeline-walks.html +++ b/docs/pipeline-walks.html @@ -169,7 +169,7 @@

Migration Legacy source code existsP KickoffScaffold the project. Answer every intake question. Propose the entry mode with evidence and record it CONFIRMED. Gate: every intake question answered. Owner ba-agent.bin/init-project.sh <project>
bin/init-agents.sh <session-root>
bin/gate-check.sh <project> P 0 Triage ✋Inventory every source file. Grade sufficiency (SPECIFICATION / OUTLINE / SKETCH). Decide extract-or-not per structure and fill the coverage matrix. Scan documents. Run the CAC-1 scope brainstorm, order the slices, get sign-off in triage.md. Owner ba-agent.bin/source-sufficiency.sh init <project>
bin/source-sufficiency.sh report <project>
bin/interview-mode.sh --set … (only after the user agrees)
bin/triage-report.sh <project>
bin/source-sufficiency.sh --refresh for late files
bin/gate-check.sh <project> 0 1 AnalysisPath A: run the platform pipeline (extract, knowledge base, BRD scaffolds, reports). Path B: convert documents to Markdown and build the KB. Path C: interview the SME. Every source file must name the artifact that consumed it. Gate: source ledger clean, four extraction quality checks. CAC-1b.cd pipelines/<platform>/pipeline && npm install
node run.js · node run.js 2 · node run.js 3 · npm run reports
bin/html-to-md.sh <project> · bin/images-to-md.sh
bin/source-ledger.sh report <project>
bin/source-ledger.sh check <project>
bin/extraction-report.sh <project>
bin/gate-check.sh <project> 1 - 2 RequirementsTurn scaffolds into validated BRDs, one per feature. Drive blocking open questions to zero. Lock facts. CAC-2 BRD sign-off, CAC-3 architecture entry.bin/brd-report.sh <project>
bin/open-questions.sh <project> --stage 2
bin/questions-report.sh · bin/facts-lock.sh · bin/question-kinds.sh
bin/gate-check.sh <project> 2 + 2 RequirementsTurn scaffolds into validated BRDs, one per feature. Drive blocking open questions to zero. Lock facts. CAC-2 BRD sign-off, CAC-3 architecture entry.bin/brd-report.sh <project> [--stage N]
bin/open-questions.sh <project> --stage 2
bin/facts-lock.sh · bin/question-kinds.sh
bin/gate-check.sh <project> 2 3 Design ✋Module boundaries, blueprint, fit-gap, security model, integrations. Design system and one wireframe per screen. CAC-4. Owner architect-agent, interviews by ba-agent.skills: modularize-domain.md, architecture-blueprint.md, design-artifacts.md
outputs: architecture/blueprint.html, design/design-system.html, design/wireframes/*.html
bin/gate-check.sh <project> 3 4 Build plan ✋Dependency-ordered script sequence, coverage ledger, one brief per module. CAC-5, then the build-ready wiring check.bin/coverage-check.sh
bin/gate-check.sh <project> 4
bin/gate-check.sh <project> build-ready 5 BuildWalking skeleton, then the five-stage module loop per module (see below). Coherence pass every two or three modules../mxcli check s.mdl -p app.mpr --references
bin/exec.sh s.mdl · ./mxcli lint -p app.mpr
project-bin/verify-module.sh <Module>
./mxcli graph-report -p app.mpr
bin/gate-check.sh <project> 5 diff --git a/docs/windows-test-run.md b/docs/windows-test-run.md index ba64dda7..08022bc1 100644 --- a/docs/windows-test-run.md +++ b/docs/windows-test-run.md @@ -124,7 +124,7 @@ I3 node project-bin/page-fidelity.js --no-log tests/wave2/fixtures/page-fidelit I4 node project-bin/page-fidelity.js --no-log tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html DemoList tests/wave2/fixtures/page-fidelity-mocks/demo-list.mdl I5 bash bin/html-to-md.sh "tests/wave2/fixtures/html-to-md/Fleet Portal Requirements.html" > ~/mxtk-win-run/fleet.md Then: wc -l ~/mxtk-win-run/fleet.md -I6 bash bin/brd-report.sh --help ; bash bin/triage-report.sh --help ; bash bin/questions-report.sh --help +I6 bash bin/brd-report.sh --help ; bash bin/triage-report.sh --help (exit 0 and a usage text each = PASS; these are the report tools with no committed input) I7 bash bin/bug-lookup.sh CE0053 I8 bash bin/token-burn.sh --help diff --git a/process/improvement-plan-e2e-reporting.md b/process/improvement-plan-e2e-reporting.md index ddd401b6..31bc6448 100644 --- a/process/improvement-plan-e2e-reporting.md +++ b/process/improvement-plan-e2e-reporting.md @@ -270,7 +270,7 @@ These close options left open in Findings 1–4 and set the constraints for the 2. **The number stops lying instead.** The report leads with coverage, not pass-rate: *"58% of checks did not run"* above the fold, and no percentage anywhere may use a denominator that excludes faults. This blocks nobody and is the only thing that actually failed. - Precedent already in-repo: `bin/questions-report.sh:72` refuses to render a clean-looking zero. + Precedent already in-repo: `bin/brd-report.sh` (exit 3) refuses to render a clean-looking zero (it took over from the retired `questions-report.sh`). 3. **Plumbing before gating.** Findings 5–8 are fixed first, so that any later signal fires on instruments that are actually present. 4. **Four fallback levels for the ledger check**, keyed on what is present rather than declared diff --git a/project-bin/assemble-prototype.js b/project-bin/assemble-prototype.js index a4b1ae58..ca6e1fe0 100755 --- a/project-bin/assemble-prototype.js +++ b/project-bin/assemble-prototype.js @@ -283,6 +283,11 @@ const SCRIPT = ` })(); `; +// The process map (process-map.js, skills/process-map.md) is built from the same wireframes and shows +// each on its step. When the project has a process model, the prototype links to the map beside it +// and the map links back (its header). Keyed on the model, not on the built map, so the order of +// the two builds does not matter. +const hasMap = fs.existsSync(path.join(path.dirname(path.resolve(outFile)), 'process-model.json')); const index = screens.map(s => '
  • ' + esc(s.title) + ' #/' + esc(s.route) + '
  • ').join('\n'); const out = '\n' + @@ -294,7 +299,8 @@ const out = '\n' + '\n' + '\n\n' + '\n' + + '' + + (hasMap ? '
    Process map: where each screen sits in the work' : '') + '\n' + '\n' + '
    \n' + screens.map(screenSection).join('') + '
    \n' + '\n\n'; diff --git a/project-bin/process-map.js b/project-bin/process-map.js index 9957a6a1..8e4d8394 100644 --- a/project-bin/process-map.js +++ b/project-bin/process-map.js @@ -23,8 +23,17 @@ // process-map.js validate [--strict] check the model (and anchors, when present) // process-map.js anchor derive anchors mechanically, keep judged ones, write // the anchors file, list what is still unanchored +// process-map.js migrate a model whose "anchors" key points at an old path (say the +// prototype folder): move the file to design/question-anchors.json, +// keep the old one as *.bak, rewrite the key, print what carried over // process-map.js build validate, then write design/process-map.html +// process-map.js build --embed the same maps as a bare page for an