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
18 changes: 14 additions & 4 deletions bin/fde.js
Original file line number Diff line number Diff line change
Expand Up @@ -2063,10 +2063,11 @@ function findDuplicateOpenRisks(eng) {
function collectDoctorIssues(eng) {
const issues = []
const s = computeSignals(eng)
const datedBlob = [
// stripTemplateNoise: a dated example inside a template comment is not work.
const datedBlob = stripTemplateNoise([
readEng(eng, 'decisions.md'), readEng(eng, 'delivery.md'),
readEng(eng, 'risks.md'), readEng(eng, 'stakeholders.md'),
].join('\n')
].join('\n'))
const hasDatedWork = /\[\d{4}-\d{2}-\d{2}\]/.test(datedBlob)
// Day-1 empty templates are not hygiene failures - nagging there trains people to ignore doctor.
const fresh = !hasDatedWork && (s.phase === '?' || s.phase === 'unset') && !s.openRisks
Expand Down Expand Up @@ -2249,6 +2250,15 @@ function stripTemplateNoise(md) {
.replace(/\*\([^)]*\)\*/g, '')
}

// Drop "**Label:** allowed · values" guidance lines. A receipt is a dated line or a
// table row - never a bold label - so template prose that *documents* a receipt must
// not satisfy the gate requiring one. hasOperatingMapContent already skips these.
function stripLegendLines(md) {
return String(md || '').split('\n')
.filter(l => !/^\s*\*\*[^*]+:\*\*/.test(l))
.join('\n')
}

const VALUE_BUCKET_RE = /(cost[- ]?save|risk[- ]?mitigat|revenue[- ]?uplift)/i

function hasValueBucket(eng) {
Expand All @@ -2257,7 +2267,7 @@ function hasValueBucket(eng) {
if (bucketLine && VALUE_BUCKET_RE.test(bucketLine[1].trim())) return true
if (!/\*\*Primary value bucket:\*\*/i.test(success) && VALUE_BUCKET_RE.test(success)) return true

const ledger = stripTemplateNoise(sectionBody(readClean(eng, 'delivery.md'), 'Value ledger') || '')
const ledger = stripLegendLines(stripTemplateNoise(sectionBody(readClean(eng, 'delivery.md'), 'Value ledger') || ''))
const table = parseMdTable(ledger)
if (table) {
const bIdx = colIndex(table.headers, /bucket/i)
Expand Down Expand Up @@ -2322,7 +2332,7 @@ function hasEvalReceipt(eng) {
if (/\bLast run:\s*\d{4}-\d{2}-\d{2}/i.test(e)) return true
if (/\|\s*G\d+\s*\|[^|\n]+\|[^|\n]+\|[^|\n]+\|[^|\n]+\|\s*pass\s*\|/i.test(e)) return true
}
const del = stripTemplateNoise(readClean(eng, 'delivery.md'))
const del = stripLegendLines(stripTemplateNoise(readClean(eng, 'delivery.md')))
if (/#{1,6}\s+Eval\b/i.test(del) && /\b(pass|SHIP|\d+\/\d+)\b/i.test(sectionBody(del, 'Eval') || del)) return true
if (/\beval (pack|receipt)[:\s].*\b(pass|SHIP)\b/i.test(del)) return true
const receipts = sectionBody(del, 'Ship receipts') || ''
Expand Down
6 changes: 4 additions & 2 deletions bin/lib/trust.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ function createTrustApi(deps) {
const t = raw.trim()
if (!t || t.startsWith('<!--') || /^#{1,6}\s/.test(t)) continue
if (/risk\s*\|\s*status|mitigation/i.test(t) || /^\|?[\s|:-]+$/.test(t)) continue
// Bullet risk with substance (skip empty "- " stubs).
if (/^[-*]/.test(t)) {
// Bullet risk with substance (skip empty "- " stubs). The bullet marker
// must be followed by space: "**Status:** open · closed" is a legend, and
// counting it as a risk reported one open risk on an empty register.
if (/^[-*]\s/.test(t)) {
if (t.replace(/^[-*]\s+/, '').trim()) n++
continue
}
Expand Down
17 changes: 17 additions & 0 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ After confirm, `fde ingest apply` writes thin dated facts into `.fde/` (same rou
| `risks.md` | Live risk register | plan, build, rescue, `fde debrief` |
| `delivery.md` | Value ledger (bucket → promised → measured → accepted by → evidence) + ship receipts + status memos | build, ship, status, close, `fde debrief` |

### Decision entries

`fde log decision "<text>"` appends a dated one-liner, which is enough for most
choices. When the reasoning is the part that has to survive the engagement,
write the long form under `## Decision log` instead:

```markdown
### [2026-08-27] Keep the existing Stripe connector
- Context: what prompted this decision
- Options considered: what was on the table
- Decision: what was chosen
- Rationale: why this over the alternatives
- Owner: who approved (customer-side name)
```

The rationale is the line a successor cannot reconstruct from the code.

## Incidents and handoff

| File | Purpose | Written by |
Expand Down
4 changes: 2 additions & 2 deletions templates/.fde/assumptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

| # | Assumption | Blast radius | How we test | Status | Evidence |
|---|------------|--------------|-------------|--------|----------|
| 1 | *(seed from brief - stated, unverified)* | CRITICAL / LOAD-BEARING / CONVENIENCE | | OPEN | |

**Status values:** `OPEN` · `TESTING` · `CONFIRMED` · `DISPROVED` · `PARKED`
**Blast radius:** `CRITICAL` · `LOAD-BEARING` · `CONVENIENCE`
**Status:** `OPEN` · `TESTING` · `CONFIRMED` · `DISPROVED` · `PARKED`

**Rule:** a CRITICAL assumption still OPEN blocks plan. DISPROVED → update `reality.md` / `success.md` and log the reset in `decisions.md` the same day.
12 changes: 2 additions & 10 deletions templates/.fde/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,5 @@

## Decision log

<!-- Use this format for each decision. The rationale is the part that survives the engagement. -->

<!--
### [Date] Decision title
- Context: what prompted this decision
- Options considered: what was on the table
- Decision: what was chosen
- Rationale: why this over the alternatives
- Owner: who approved
-->
<!-- Dated lines: what was chosen, and why over the alternatives. Long form: docs/schema.md
- [2026-07-18] kept the vendor connector over an in-house rewrite - Priya wants the Q3 audit clean first -->
8 changes: 6 additions & 2 deletions templates/.fde/delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@

| Date | Slice | Bucket | Promised | Measured | Accepted by | Evidence | Rollback |
|------|-------|--------|----------|----------|-------------|----------|----------|
| | | *(cost-save / risk-mitigation / revenue-uplift)* | *(what we said it would change)* | *(what actually changed, or pending)* | *(customer-side name + date, or it stays claimed)* | *(who/when/metric)* | |

**Bucket:** `cost-save` · `risk-mitigation` · `revenue-uplift`
**Accepted by:** a customer-side name and date - without one the value stays *claimed*, not accepted.

## Ship receipts

<!-- Fill before green ship. Eval = n/a unless AI touches the slice. -->

| Date | Slice | Audit receipt | Eval receipt |
|------|-------|---------------|--------------|
| | | *(dated cite: exceptions/operating path verified — terrain/reality/audit)* | *(n/a \| evals.md pass + HITL owner)* |

**Audit receipt:** dated cite that the exception/operating path was verified (`terrain.md` / `reality.md`).
**Eval receipt:** `n/a` unless AI touches the slice, else `evals.md` pass + the HITL owner.

## Shipped

Expand Down
4 changes: 0 additions & 4 deletions templates/.fde/evals.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@

| ID | Input (sanitized) | Expected | Pass rule | Last run | Result |
|----|-------------------|----------|-----------|----------|--------|
| G1 | | | | | |

## Failure modes

| Mode | How it shows up | Detection | Mitigation |
|------|-----------------|-----------|------------|
| | | | |

## Pass / fail (this ship)
- **Golden:** _/_ pass (threshold: _)
Expand All @@ -31,12 +29,10 @@

| Decision / action | Autonomous OK? | Reviewer role | Escalation |
|-------------------|----------------|---------------|------------|
| | | | |

**HITL rule for this ship:**

## Change log

| Date | What changed | Pack re-run? | Notes |
|------|--------------|--------------|-------|
| | | | |
4 changes: 3 additions & 1 deletion templates/.fde/risks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

| Risk | Status | Owner | Mitigation |
|------|--------|-------|------------|
| | open | | |

**Status:** `open` · `mitigating` · `closed` · `accepted`
<!-- `fde log risk "<text>"` appends a dated line; the table is for live, owned risk. -->
3 changes: 2 additions & 1 deletion templates/.fde/stakeholders.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

| Name | Role | Stance | Notes |
|------|------|--------|-------|
| | | champion / neutral / resistant | |

**Stance:** `champion` · `neutral` · `resistant`

**Trust signal:** green | amber | red
**Last trust check:**
Expand Down
2 changes: 0 additions & 2 deletions templates/.fde/terrain.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

| Exception / break | Who notices first | What they do today (workaround) | System of record then | Blast if wrong | Evidence |
|-------------------|-------------------|---------------------------------|-----------------------|----------------|----------|
| | | | | | |

**Shadow systems / silent workarounds:**
**Sacred / untouchable in ops:**
Expand All @@ -23,4 +22,3 @@

| Step | Deterministic | Model judgement | Human approve |
|------|---------------|-----------------|---------------|
| | | | |
48 changes: 48 additions & 0 deletions test/fde-cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,28 @@ test('triage hygiene: silent on fresh day-1; speaks after real work accrues gaps
assert.doesNotMatch(clean.stdout, /hygiene:/i, 'hygiene must be silent when doctor is OK')
})

test('a day-1 risk register counts zero open risks; a legend line is not a risk', () => {
const sandbox = makeSandbox('risk-legend')
assert.equal(runFde(sandbox, ['resume', '--init', 'legendco']).status, 0)
const shipped = runFde(sandbox, ['triage'])
assert.equal(shipped.status, 0, shipped.stderr)
assert.match(shipped.stdout, /open risks:0/, 'shipped risks.md template must count 0 open risks')

// "**Status:** open · closed" starts with '*' - it is markdown emphasis, not a bullet.
const eng = engagementPath(sandbox, 'legendco')
fs.writeFileSync(
path.join(eng, 'risks.md'),
'# Risk register\n\n| Risk | Status | Owner | Mitigation |\n|------|--------|-------|------------|\n\n' +
'**Status:** `open` · `mitigating` · `closed` · `accepted`\n'
)
const legend = runFde(sandbox, ['triage'])
assert.match(legend.stdout, /open risks:0/, 'a status legend must not read as an open risk')

assert.equal(runFde(sandbox, ['log', 'risk', 'cutover window still unsigned']).status, 0)
const real = runFde(sandbox, ['triage'])
assert.match(real.stdout, /open risks:1/, 'a logged risk still counts')
})

test('phase ship/close warns when open risks remain', () => {
const sandbox = makeSandbox('phase-risk-warn')
assert.equal(runFde(sandbox, ['resume', '--init', 'phasewarn']).status, 0)
Expand Down Expand Up @@ -1629,6 +1651,32 @@ test('doctor ship/close: value bucket required; eval only when AI in scope', ()
assert.equal(aiOk.status, 0, aiOk.stdout + aiOk.stderr)
})

test('the shipped delivery.md template cannot satisfy the eval-receipt gate it documents', () => {
const sandbox = makeSandbox('eval-gate-template')
assert.equal(runFde(sandbox, ['resume', '--init', 'aico']).status, 0)
const eng = engagementPath(sandbox, 'aico')
fs.appendFileSync(
path.join(eng, 'trust-profile.md'),
'\n## AI policy\nCustomer approved an LLM-assisted reranker; no PHI may reach the model.\n'
)
assert.equal(runFde(sandbox, ['log', 'phase', 'ship']).status, 0)

// delivery.md ships "**Eval receipt:** n/a unless AI touches the slice, else evals.md
// pass + the HITL owner" - prose that documents the receipt, not a receipt.
const untouched = runFde(sandbox, ['doctor'])
assert.notEqual(untouched.status, 0)
assert.match(untouched.stdout, /eval receipt/i, 'day-1 template prose must not satisfy the gate')

// A dated Ship receipts row is a receipt, and still clears it.
const del = fs.readFileSync(path.join(eng, 'delivery.md'), 'utf8').replace(
/(\| Date \| Slice \| Audit receipt \| Eval receipt \|\n\|[-|]+\|)/,
'$1\n| 2026-07-18 | reranker v1 | 2026-07-10 exception path verified with Tom | evals.md 18/18 pass, HITL owner Priya |'
)
fs.writeFileSync(path.join(eng, 'delivery.md'), del)
const receipted = runFde(sandbox, ['doctor'])
assert.doesNotMatch(receipted.stdout, /eval receipt/i, 'a dated ship receipt clears the gate')
})

test('doctor calls out a measured benefit nobody on the customer side accepted', () => {
const sandbox = makeSandbox('doctor-claimed-value')
assert.equal(runFde(sandbox, ['resume', '--init', 'claimco']).status, 0)
Expand Down
Loading