release: v0.37.0 - #486
Merged
Merged
release: v0.37.0#486
Conversation
… boolean
`forgeplan get` returned about twenty fields and none of them were links, so an
artifact with five edges and an orphan rendered identically. The issue reports
that costing a wrong conclusion in a live session: a spec was called orphaned on
the strength of a response that simply never carries the answer.
The data was already there. `get.rs` fetched both edge sets, reduced them to
`has_links` to pick a hint, and dropped them one line before the output. Nothing
had to be looked up that was not being looked up already.
Emitted unconditionally, as `{"outbound": [], "inbound": []}` when there are
none. Against a rich object an absent field reads as "this artifact has none",
not "this tool does not report them" — an empty array is the difference between
an answer and a silence.
Inbound is reported separately because it is the half `forgeplan graph | grep
"<id> -->"` cannot answer, and it is the one behind "which evidence supports
this" when r_eff is 0.
MCP `forgeplan_get` gets the same field. The `From<ArtifactRecord>` conversion
has no store handle, so the handler fills it; the DTO field is non-optional so a
caller who forgets still emits empty arrays rather than dropping it silently.
Three tests on the real binary against a real workspace — the defect was
invisible to unit tests because every layer worked, the output just did not
carry the answer.
Closes #447
…lways empty The rule warned that an artifact's `## Related Artifacts` table names targets the frontmatter `links:` array does not reference. It could not do otherwise: `frontmatter_map()` rebuilds a frontmatter from the record's columns, the record has no link columns, so the map never carries `links`. The comparison set was empty for every artifact, always. The consequence is worse than a false positive. The warning fired on artifacts that were correctly linked, named the very targets the graph showed as edges, and could not be silenced by linking anything — only by deleting the table. A SHOULD warning nobody can close is a warning everybody learns to skip, and it fires on essentially every well-linked artifact. Measured on SPEC-003 in this workspace, before: eight ids named, seven of them false — three were real edges visible in `forgeplan graph`, four were `FR-1` through `FR-5`. After: one, `EPIC-007`, which is true; the graph has no such edge. Two defects, both closed here. Links now come from the relations table — the same source `forgeplan graph` renders and `forgeplan link` writes — via `LanceStore::frontmatter_map_with_links`, used at all three sites that run the full rule set. So the warning agrees with what the reader can check by eye. The extractor's `[A-Z]+-[0-9]+` regex matched any token of that shape, sweeping up requirement numbers (`FR-1`) and invariant numbers (`I-3`). Those can never be link targets, so the finding was unclosable by construction and its remediation `forgeplan link <this-id> FR-1` was unrunnable — a PRD-071 violation of the same shape as #348 and #351. Only prefixes that map to a real artifact kind survive. The remediation also stopped shipping `<this-id>` and `<target>` for the reader to substitute: both ids are known at that point. The relation stays a choice because it genuinely is one. PROB-059, which introduced this rule and is still active, describes a real drift problem. This says its detector never worked. Closes #446 Refs: PROB-059
The block said the leaked HINDSIGHT_API_KEY was rotated. That has not been confirmed. A repo file asserting a mitigation that may not have happened is worse than no comment: the next reader takes it as settled and stops looking. It now says only what is known — the object stays fetchable by SHA, so rotation is the only thing that closes it, and this block does not claim it happened.
PROB-100 records the shared root cause behind #446 and #447 — the artifact record carries no links, so the validator warned about links that exist and `get` reported an orphan for a linked artifact. It refines PROB-059, which introduced the drift rule and is still active: the drift PROB-059 describes is real, and this says its detector never worked. EVID-168 carries the before/after measurements on real artifacts, the gate results, and the reason the three test failures are #454 rather than this change. R_eff 1.00.
…0.36.0 chore: sync main into dev after v0.36.0
Reported by a user reading the output, because nothing else was watching:
`Loading embedding model...` printed on every run, including runs where every
artifact was already current.
Measured on this workspace, 427 artifacts, all current:
before 8.22s to report "0 embedded, 427 already current"
after 0.25s (warm; three runs: 0.83 / 0.25 / 0.25)
The model was read off disk and never used.
This is the missing half of PROB-093. That fix made `embed` incremental — skip
a record whose vector exists and whose content hash still matches — and removed
13m18s of redundant encoding. But the skip was decided inside the loop, after
`Embedder::new()` had already run, so the setup for the work outlived the work.
Now the work list is built first and the model loads only if it is non-empty.
The banner appears only when a model is actually about to load, which is what a
reader already assumed it meant.
Also corrects the progress line, which counted the wrong thing:
- Embedding 427 artifact(s) ... every record, including skipped ones
+ Embedding 1 of 427 artifact(s) ...
Two tests, and the mutation check found them unequal: with the fix reverted only
`a_current_workspace_does_not_reload_the_model` fails. The empty-workspace test
passes either way, because an older early return catches that case — said so in
the test's own doc comment rather than leaving it to look like proof it is not.
Both are gated on `semantic-search`, which CI's `cargo nextest run` does not
enable, so they compile there and execute only locally. That gap is PROB-102 and
is the reason this defect reached a user first.
Refs: PROB-103, PROB-093, PROB-102
PRD-086 gathers four defects that share one shape: the trust layer reports values it never computed. #325 asks an EvidencePack for its evidence when the pack IS the evidence, and the intrinsic formula already exists in `score_evidence` — it is simply never applied to the pack itself. #393 has the anomaly detector printing R_eff=0 for nonzero scores, labelling depth-cap give-ups as "cycle or depth cap" in graphs with zero cycles, and giving up where the scorer resolves the weakest link fine. #330 is `advance_phase` with no monotonicity guard, walking a shipped artifact back from `done` on the MCP path. #392 is narrowed to its one real half. The Non-Goals matter as much as the requirements: the weakest-link formula is not weakened. The three fixes proposed in #392 — local-only scoring, one-hop propagation, a floor at self_score — are each an average in disguise, and ADR-002 already settled this shape for `draft` dependencies by skipping them rather than softening the min. Three findings surfaced while investigating, each recorded rather than folded into the PRD, because two of them need a decision this PRD does not make: PROB-101 — an EvidencePack with no structured fields at all scores its target 1.00. `CLAUDE.md` RED LINE #7, the `/forge` skill shipped to every user, and EVIDENCE-PROTOCOL all promise CL0 and 0.1. The code defaults absent fields to CL3 deliberately. The incentive is inverted: writing `congruence_level: 2` honestly scores 0.9, writing nothing scores 1.0. 3 of 166 packs here are affected; changing it is a breaking scoring change and needs a call. PROB-102 — `embedding_reference.rs`, whose stated purpose is catching the one failure mode an engine swap has that nothing else would, runs zero tests in CI. `cargo check` and `clippy` pass `--features semantic-search`; `cargo nextest run` does not. The oracle written for the v0.35.0 ONNX-to-tract swap never ran during it. `0 passed` reads as green. PROB-103 — the embed cold start, fixed in the previous commit, with the measurements. Refs: PRD-086, PROB-101, PROB-102, PROB-103
…the rest `carry_identity_forward` re-attached exactly `["slug", "predicted_number", "assigned_number"]` from the old body when the replacement did not declare them. Those are the three fields whose loss someone noticed. The argument that justified carrying them — the body's own frontmatter block is collateral damage of any body replacement — applies verbatim to every other field in that block, so the constant was a fix for three instances of a class while the rest of the class kept being deleted in silence. Measured on a real workspace, not deduced: a NO-OP round-trip of RFC-022 — read the body back with `forgeplan get --json`, hand it straight to `update --body` unchanged — dropped eight frontmatter lines. Five of them (`author`, `depth`, `id`, `status`, `title`) are in `KNOWN_FM_KEYS` and are regenerated into the projection block above, so losing the in-body copy is correct. Three were pure loss: `created`, `updated`, `prd`. The rule is now the one the neighbouring `filter_preserved` already applies to the projection block's unknown keys (PRD-057 FR-009): anything the record does not own survives. Derived from `KNOWN_FM_KEYS` rather than a second authored list, so a field added to the record is excluded automatically and a field added to the body is preserved automatically — neither needs anyone to remember this function exists. Carrying a `KNOWN_FM_KEYS` field would be worse than dropping it: `update_body_with_projection` parses `status` back out of the new body to sync LanceDB, so a stale in-body copy could resurrect a superseded status. The test asserts that direction too — "carry everything" would pass the first half and fail the second. Consequence, stated rather than left to be discovered: a caller can no longer delete a body-frontmatter field by omitting it. Deliberate — silent data loss is the failure this function exists to prevent and is the more expensive of the two. Record-owned fields have dedicated mutators (`--status`, `--title`, `--depth`). Verified by mutation: restoring the three-key list turns the new test red on exactly the right assertion — "`created: 2026-09-03` was dropped — it is not a record-owned field" — while the four pre-existing `carry_identity_forward` tests stay green, so the old guarantees are preserved and the new test detects this defect specifically. Reverted; no probe left in the tree. `cargo test -p forgeplan-core --lib projection::` — 133 passed, 0 failed. `cargo clippy -p forgeplan-core --lib` — clean. Two pre-existing problems found while verifying, NOT touched here and not caused by this change: - `crates/forgeplan-core/tests/integrity_test.rs` does not compile on a clean tree (8 errors: `add_relation_for_test`, `create_artifact_for_test` do not exist on `LanceStore`). - `git::tests::*` are order-dependent under the full `--lib` run. Identical clean-tree runs gave 1 failed and then 4 failed; run alone the group is 51/51 green. Flaky, not a regression — established by re-running the same code, not by assuming. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vNext audit blocked the program on decisions no document could make. The owner made the first; the other two were delegated for a worked proposal. Both ADRs are drafts — proposals on the record, not activated policy. ADR-025 — orchestration sits above ForgePlan. ADR-001 reaffirmed; ADR-009's orchestrator clause superseded, its marketplace model untouched. Carries the per-surface disposition the audit demanded instead of a blanket boundary: projections stay (dispatch/order/blocked/progress), claim/release stays but reframed as artifact write-locks rather than assignment, phase is absorbed into lifecycle with no new investment (its state is already per-machine), estimate/calibrate and the playbook runtime move to extensions, and the memory kind is deprecated — 2 artifacts exist, creation is broken, #411 closes by removal. ADR-026 — storage classes for machine-written records. One rule: git-tracked if a human must review it or another machine must trust it; local if it is raw per-machine material; referenced if another system owns it. Contracts, bundles and verdicts are Class A (tracked, append-only, digest-linked, RED LINE #11 extends to them); receipts and the audit stream are Class B (local, promoted into evidence by extract + digest); CI and deployment state are Class C (referenced, never copied). Framed as an amendment to ADR-003: the load-bearing idea was never markdown, it was versioned plain files as truth with disposable indexes. The ADR-001/003/009 diffs are the backlinks forgeplan_link writes into the target artifacts. Refs: ADR-025, ADR-026, ADR-001, ADR-003, ADR-009
…ion style The owner reviewed both boundary decisions in plain-language form and approved them. EVID-169 records the basis — the vNext audit measurements each decision rests on, plus the approval itself — so activation does not rest on the author's say-so. Both ADRs and the pack are now active, R_eff 0.70. AGENTS.md gains "How to explain things to the owner": the format that got the decisions understood, made a rule. Collision before answer, one live example over three definitions, minimal anglicisms, consequences not just verdicts, metaphors only when both halves map to real subsystems. The trigger was a direct comparison — the same decisions explained in architecture-speak did not land; the plain version did. The same rule now also lives in the user's global agent config, so it is recorded here only as far as this repository's agents are concerned. Refs: ADR-025, ADR-026, EVID-169
Formatting only — `cargo fmt` wrapped the multi-line `assert!` in `carry_identity_forward_keeps_every_field_the_record_does_not_own`. No logic change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five defects, one shape. Each returned a plausible number, which is why all five survived — a zero looks like an honest zero. Evidence that declared nothing scored full marks. A pack with no `verdict` and no `congruence_level` gave the artifact it informs 1.00. CLAUDE.md RED LINE #7, the /forge skill that setup-skill installs, and EVIDENCE-PROTOCOL.md all state CL0 and 0.1 — the binary and every document describing it disagreed, in the inflating direction. The incentive was backwards too: `congruence_level: 2` honestly written scored 0.9, writing nothing scored 1.0. An unrecognised verdict fell through to Supports for the same reason, while congruence_level in the same function already failed closed with a warning. Both now fail closed and log why. A leaf EvidencePack scored zero (#325). The scorer asked a pack for its evidence, found none — a pack has no packs — and returned 0.0. The intrinsic score already existed and is applied to that same pack whenever it scores for something else; it is now applied to the pack itself. Its outgoing `informs` edges are also excluded from its own dependency walk, because trust flowing from a decision down into the measurement that justifies it is backwards. An exempt Note poisoned everything built on it (#392, narrowed). The routing table calls a Note the artifact for trivial reversible work — no evidence — and the cascade then read an active unevidenced Note as zero trust. Two of the product's own rules contradicting each other. `note` and `memory` are skipped with a logged factor, exactly as ADR-002 skips non-active dependencies. The weakest-link formula is untouched for kinds that can owe evidence, and a test pins that an unevidenced PRD still drags its dependants down. `advance_phase` walked phases backwards (#330). No monotonicity guard, and MCP forgeplan_validate calls it on every PASS — so validating a shipped artifact reset it from `done` and health then reported a mismatch it did not have until someone checked it. Refused with an explanation; the explicit operator path keeps both directions. The anomaly detector reported three things it never checked (#393): a literal 0.0 instead of the stored score (now `r_eff_cached`, named so because the reporter's confusion came from comparing it against a fresh run), `cycle or depth cap` on every give-up in a graph with zero cycles, and an ancestor walk that followed edges the scorer skips. All three fixed at source. Verification. Every fix has a test that fails when the fix is reverted — checked, not assumed. Two of those tests failed that check and were rewritten: one passed on the broken code, and its first "strengthened" version asserted something false. Both stories are in the test file. Dogfooding on the real graph then caught what none of the tests read: `score` printed "No evidence linked. R_eff = 0.0" directly above a factor line saying 1.00. Its display branch was keyed on a condition that used to mean one thing. Fixed and verified across all four cases. BREAKING: packs missing either field drop from 1.0 to 0.1, and artifacts whose weakest link was such a pack drop with them. Run `forgeplan score --all` after upgrading. Here that is 3 of 167 (EVID-033/034/035 → PROB-014, PROB-016, RFC-004); elsewhere unknown. The drop is the correct reading — those scores were never earned. 3307 passed, 3 failed across 94 binaries. The 3 are #454: all in `git::tests`, none of these commits touch that module, all 51 pass single-threaded. Refs: PRD-086, PROB-101, EVID-170, #325, #330, #392, #393
…ixes Five review dimensions, every finding handed to a separate agent told to refute it: 10 confirmed, 9 refuted. Three of the confirmed ones were introduced by the PRD-086 commit itself, all of the same class it exists to close — a surface reporting something it never computed. hints.rs — `forgeplan score` on a canonical leaf pack printed "R_eff = 1.00" and then, three lines lower, "No evidence linked — R_eff will be 0.0". The display branch in score.rs was fixed; this one was missed because no test reads hint text. The warning states a number, so it is guarded on the score rather than on the artifact kind: any future case where a nonzero score arrives without linked children stays honest without this function learning what kinds exist. context.rs — same defect, worse consequence. Its `Next:` told the agent to create an EvidencePack for an EvidencePack, and `Next:` is a PRD-071 contract line an agent is obliged to run. The cost was phantom artifacts, not a confusing sentence. Both the hint and the suggestion list are guarded now. anomalies.rs, two defects, both mine. The note/memory skip was applied when a node was popped, but the acceptance test asks whether every parent has been visited and a skipped parent never gets visited — so a node that IS the weakest link but sits behind a note failed the test, was never named, and the detector said "the cause is local" while `forgeplan score` resolved the same chain and named the artifact. Two walks disagreeing again, which is exactly what FR-006 was supposed to stop. The skip is applied at parent-collection time now. Second: `hit_revisit` fires on ordinary re-convergence in an acyclic graph — the push guard checks `visited`, not frontier membership — and the message turned that into "the chain loops". That is #393 bug 2 repeated in narrower form. The wording now states what was observed and points at `forgeplan blocked` for whether real cycles exist. MCP server.rs — the CLI moved to the unchecked entry point so an operator can still correct a phase downward; the MCP tool did not, so the two surfaces disagreed about the same operation, its description still advertised out-of-order jumps, and the error branch answered a monotonicity refusal with "check the directory is writable". The guard belongs on automatic advancement (`maybe_advance_phase`), not on an explicit call someone made. And the finding that cost the most to fix: `a_pack_is_not_dragged_down_by_the_ artifact_it_supports` never reached FR-002. The FR-001 early return fires first and the dependency walk is never built, so deleting the filter left all five tests green. The replacement took three attempts, and the two failures are recorded in the test because both looked right: 1. a leaf pack informing an unevidenced PRD — early return, walk never built 2. a pack WITH children informing an unevidenced PRD — the PRD is not unevidenced, because evidence collection reads incoming edges, so linking the pack to the PRD is what evidences it The working version puts the weakness two hops away: PRD-201 unevidenced, PRD-200 based_on it and therefore zero despite its own evidence, EVID-200 informs PRD-200. Mutation-checked — removing the filter fails exactly one test. AGENTS.md gains the build-directory rule this session earned four times over: `target/` runs 20-45 GB on a disk under 20 GB free, parallel agent compilation fills it, and `errno=28` surfaces as `passed=0 failed=0` at exit 0. Written after a review workflow put 27 agents in this worktree and drove free space to 446 MB. Not fixed here, deliberately: a leaf pack with an evidence-kind neighbour reports the neighbour's score instead of its own (reff.rs:349). The verifier downgraded it from critical to low — fail-closed parsing is intact, nothing propagates to consumers, it gates nothing, and all six EVID->EVID edges in this repo are supports/CL3 pointing at supports/CL3 so the substituted value equals the true one. Fixing it means changing evidence collection, which is not this PR's scope. 3312 passed, 1 failed across 94 binaries. The failure is #454: c34_forgeplan_generate_no_llm_smoke asserts that NO LLM provider is configured and breaks when a sibling test sets the variable; it passes in isolation and no line of this diff touches that path. Refs: PRD-086, #325, #330, #392, #393
…#470 A leaf pack with an evidence-kind neighbour reports the neighbour's score instead of its own, because the FR-001 guard asks whether the linked-evidence set is empty and that set counts every evidence neighbour in either direction. Recorded rather than fixed. A refuting agent measured the claims that made it look critical and they do not hold: fail-closed parsing is untouched, nothing propagates to the artifacts a pack informs, activation is not R_eff-gated, and all six EVID-to-EVID edges in this graph are supports/CL3 pointing at supports/CL3 so the substituted value equals the true one. Most of it predates PRD-086, which only introduced the 0.10 baseline that makes the jump visible. Carries the measurements, the two candidate fix shapes, and the regression test to write first — a refutes/CL3 pack linked to a supports/CL3 pack must not report 1.00. Refs: PROB-104, PRD-086
… ordering test CI caught what the local run did not: `concurrent_advances_all_succeed` failed on the runner and passed here. Not a flake — the defect is deterministic, only its visibility depends on scheduling. Sixteen tasks oscillate Code <-> Test, and with the #330 guard in place the losers of that race are refused, so `.expect("advance_phase failed")` panics. The test's own comment says what it is for: tmp-filename uniqueness under concurrent writes in the same tokio tick. The oscillation is only a way to make all sixteen tasks write. Guarding phase order silently changed what the test measures, so it moves to `advance_phase_unchecked` — the same treatment `history_is_capped_fifo` already needed, and for the same reason. Rewriting it that way would have dropped the question nobody was asking: does the CHECKED path behave under concurrency? `concurrent_checked_advances_are_ safe` now covers it — sixteen tasks all advancing FORWARD to the same phase, so every call is either a real transition or a no-op and none is a regression. All must succeed, and the state must land exactly there. Audited the two remaining `advance_phase` callers in tests (`health_bench.rs:101`, `verdict_cli_vs_mcp_consistency_test.rs:129`). Both initialise at Shape and take a single forward step, skipping the `target == Shape` case, so neither can regress. No other hidden failure of this shape exists. 3312 passed, 2 failed locally across 94 binaries; both failures are #454 in `git::tests`, which pass in isolation and which this diff does not touch. Refs: #330, PRD-086 FR-007
…real specs Measured on 0.36.0 before writing any code, because issue #450's premise did not survive contact with this repository. An untouched SPEC template — body still `{METHOD} /v1/{resource}` — validated with 0 errors and 0 warnings, and once any evidence pack was linked it activated at R_eff 1.00. A complete behavioural spec with two requirements and two GIVEN/WHEN/THEN scenarios failed a MUST rule and could not activate at all. The validator waved through a document saying nothing and blocked one carrying a full test oracle. Two independent causes. check_stub, the gate whose whole job is catching unfilled templates, knows twelve phrases and all twelve are PRD prose ("Что мы строим и почему это важно", "[Actor] can [capability]"). None appears in the SPEC template. It also counted placeholders but capped that at +1 against a threshold of 3, so fifteen unfilled slots weighed the same as one. The count now scales, with the threshold taken from the corpus rather than taste: SPEC template 15 placeholders, PRD template 5, the six real SPECs 0-3. A line-count test was measured and rejected — the untouched template has 25 non-empty lines under `## API Contracts`, because placeholder JSON is still lines. spec-contracts accepted only API Contracts / Data Models / Contracts, all structural, so `## Requirements` + `#### Scenario` had no route through the MUST. The kernel was mandating one methodology's shape and rejecting the other — the reverse of what #450 reports. It now accepts a behavioural contract too. The rule still demands a contract; it stopped demanding one particular form. Also: the stub gate's remediation told every kind to "Fill MUST sections (Problem, Goals, FR)", meaningless for a SPEC or an ADR. The SPEC path was unreachable until now so the wrong advice was never printed; it names the sections of the kind in hand. New rules. spec-requirement-has-scenario (Should) — issue #450 narrowed to internal consistency: a spec that opens a `### Requirement` must close it with a `#### Scenario`. The blanket form was measured first and rejected — none of the six SPECs here uses those headings, so it would have fired on 6 of 6, none defective. Conditional means silent on a structural spec, firing only on a half-authored behavioural one. prd-nfr-exist and prd-nfr-measurable (both Should) — issue #449. PRD carried 24 validator rules and none for non-functional requirements, while extract_nfr_section already existed, called from exactly one place: the tech-leakage check. The subjective-adjective blacklist reads like a list of NFRs — scalable, robust, efficient, responsive, fast — and was only ever applied to the FR section. Across the 69 PRDs here: 2 hits in FR (checked), 15 in NFR (not). All 15 sit inside the template's own `<!-- BAD: "System should be fast and responsive" -->` guidance, so the rule strips non-prose first. Without that it produces 15 findings out of 15 false, closable only by deleting the template's instructions. Verified: 0 findings across all 69 PRDs, while a hand-written "NFR-001: The export is fast and the service is robust" still produces three with line numbers. Should, not Must, for all three: 30 of 69 PRDs have no NFR section, and turning them red at once is how a rule gets ignored rather than obeyed. Four rule-count tests failed and were updated rather than silenced. They sum named groups with comments instead of asserting a bare number, so updating one means explaining where the new term came from. rules_for_spec_returns_base_plus_3 was renamed to _plus_4 rather than edited in place — a name carrying a number goes stale silently. Nine new tests. Two mutation checks: remove the comment strip and nfr_adjectives_inside_html_comments_are_not_flagged fails; remove the placeholder scaling and the untouched template is back to PASS 0/0. Tests were run per crate, not with --workspace: a full workspace build does not fit in the free space on this machine and twice reported "run did not happen" with ld errno=28. core 2239 passed / 3 failed (17 binaries), cli 807/0 (58), mcp 274/0 (19) — 94 binaries total, matching what --workspace produces. The 3 are #454 in git::tests, which this diff does not touch. Refs: PROB-105, #449, #450
Context is large enough that this needs to be readable from a fresh start. Everything the document points at is committed, which is the correction to the last handoff written in this repo: that one named a scratchpad path that did not exist here, because the files were in a session temp directory tied to a session id. It handed off work that was about to evaporate while reporting it ready — the same defect class the release it accompanied was about. Carries the measurements rather than the conclusions: the placeholder counts that set the stub threshold (template 15, PRD template 5, real specs 0-3), the 2-vs-15 FR/NFR adjective split and why all 15 were false, the per-crate test results and why --workspace does not fit on this machine, and the two mutation checks. Also records what NOT to re-litigate — the rejected shape whitelist, the blanket #450 rule, Must instead of Should — and flags the uncommitted Cargo.toml and .cargo/ work belonging to a second session in this worktree, so nobody sweeps it into a commit. Refs: PROB-105, #449, #450
…l template The SPEC template is API-first and is the only signal an author has about what a spec looks like. Now that the validator accepts a behavioural contract, the template says so. The existing stub test pins a hand-abridged copy of the template, so it cannot notice the real file changing -- adding this comment proved the gap. A new test reads the file the binary actually ships (include_str!, same path as template::engine) and asserts two things: it still reads as a stub, and the guidance comment does not register as a heading. The comment names `## Requirements` inside backticks; if section_exists ever started matching those, an empty template would satisfy spec-contracts through its own comment. Both assertions mutation-checked: MANY_PLACEHOLDERS = 100 kills the first, appending a real `## Requirements` heading kills the second. Also records the #449 / #450 / PROB-105 work in CHANGELOG, and corrects the handoff's stale "not yet pushed" status. Refs: prob-105 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 4 mutation checks Records what was measured rather than what was reported: the before/after validator behaviour on the template and on a behavioural spec, the corpus numbers that set the placeholder threshold (SPEC template 15, PRD template 5, real SPECs 0-3), and the 2-vs-15 FR/NFR adjective split with all 15 inside the template's own BAD-example guidance. Four mutation checks, because a green test on broken code proves nothing. Also states this pack's limits: it certifies behaviour on this repo's corpus only, the FR-side rule still carries the same latent bug, and the 4 git::tests failures are the known #454 flake, not health. PROB-105 R_eff 0.00 -> 1.00; both artifacts activated. Refs: prob-105, EVID-171 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[PROB-105] The SPEC validator passed empty templates and blocked real specs
A finding that lives only in a reply is gone at the end of the session. That has cost real work here twice: a 465-artifact measurement that cancelled a planned fix existed only in chat, and a handoff document pointed at a scratch directory that did not outlive the session. Three kinds of finding, three homes: a behaviour-changing rule goes in this file, work goes in an open issue, and a measured dead end goes in an issue closed on arrival under the new `measured-not-planned` label. The third is the one that pays. A measurement that CANCELS work is worth as much as one that starts it and evaporates faster, because nobody files "we checked and it does not matter" — so the next person re-derives it. #476 is the worked example. Every filing carries a trigger phrased as a condition, not a date, and the revisit is a step in the release flow rather than a habit nobody has. Also records two mechanics that silently do nothing: `Closes #N` is inert when merging into `dev` because GitHub honours it only on the default branch, and an empty `gh` result can be a GraphQL EOF rather than an absence. Plus the zsh `PIPESTATUS` trap, filed with the disk-full family it belongs to — the exit code is real, it is just measuring `tail`. Refs: #476
docs(agents): give findings a home, and say which one
…he file PROB-105 described a defect that #472 fixed, so it is retired as history rather than deleted. The `## Deprecation` section here was restored by hand through `forgeplan update --body`. `forgeplan deprecate` wrote it to LanceDB only: the status reached the file, the reason did not, and `lance/` is gitignored — so on a fresh clone the reason would simply not exist while `forgeplan get` kept showing it. Filed as #478 with a reproducer on the shipped v0.36.0 binary; `reopen` loses its section the same way, and #479 covers a separate identity defect found in the same run. Refs: prob-105, #472, #478 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(prob-105): deprecate as resolved, with the reason actually in the file
…able `forgeplan deprecate <id> --reason "..."` echoed the reason back and `forgeplan get` showed it, but the markdown file never received the `## Deprecation` section -- only the status (frontmatter) projected. `renew` and `reopen` lost `## Renewal` / `## Reopened` the same way. Root cause is a collision between two individually-correct behaviours, not a stale read: `render_projection` is files-first by design (RFC-004, `force_body = false`) so a user's on-disk edits survive `link`/`tag`/ `activate`. It discards whatever body a caller hands it whenever the file already has one -- which is exactly what these three lifecycle functions do after appending a section via `store.update_body`. The loss is permanent, not a temporary disagreement: `read_file_body_ if_newer` compares content rather than mtime, so the next mutation on that artifact syncs the section-less file body back over LanceDB and erases the reason there too. `.forgeplan/lance/` is gitignored, so on a fresh clone the reason simply does not exist. Fix: the three CLI call sites now use the existing `render_projection_ with_body` (force_body = true) -- safe here because each is preceded by `sync_file_to_store`, so the DB body is the file body plus the section just appended. The MCP `deprecate` handler needed new code: `render_ projection_record` had no forcing parameter at all, so this adds `render_projection_record_with_body` / `render_after_mutation_with_ body` alongside it. `render_projection`'s default stays files-first -- flipping it would break the user-edit protection it exists for. Tests: the existing lifecycle unit tests (`lifecycle/mod.rs:748/801/ 910`) assert through the store, which is the half that was never broken -- that is why this shipped. Four new CLI integration tests read the `.md` off disk instead, including one that runs a second lifecycle command afterward to catch the permanent-loss half. A source-grep invariant (`lifecycle_body_projection_invariant.rs`) pins each call site to the forcing variant so a future edit cannot silently regress it back. Every fix mutation-tested: reverting any of the three CLI call sites or the MCP call site fails the matching test, not a coincidental one. Refs: #478 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng it
`tests/embedding_reference.rs` pins the embedding engine's output against
values captured before the v0.35.0 ONNX -> tract swap. It has run in CI
zero times since it was written: the file is entirely behind
`semantic-search`, `check`/`clippy` compile with the feature but never
execute a test, and `nextest run --workspace --all-targets` runs without
the feature at all -- the assertions were never even compiled into that
invocation. A green CI has meant nothing about whether the engine is
correct since the feature existed.
New `test-embedding-oracle` job: a dedicated `cargo nextest run -p
forgeplan-core --features semantic-search --test embedding_reference`,
with the ~2.1 GB model cached across runs via `actions/cache` keyed on
the model repo name (the `check` job's own comment says downloading it
per run "is not worth it" -- caching removes that trade-off instead of
accepting it).
A cache alone would reproduce the same defect one layer down.
`embedder_or_skip` returns `None` on a missing model and each test
`return`s early -- which nextest reports as PASS, not skipped. A cold or
broken cache in this job would go green having asserted nothing, same
shape as the bug it exists to close. `FORGEPLAN_REQUIRE_MODEL_IN_TESTS=1`
turns that `None` into a panic in this job specifically; local runs
without the var keep the quiet skip, so a developer without the model
isn't blocked from the rest of the suite.
The env-var decision itself is pulled into a pure function
(`require_model_in_tests_from`) and unit-tested without touching process
env, following the precedent `resolve_cache_dir_from` already set for
the same "env mutation in tests is unsafe in 2024, take it as an
argument" reason.
Verified, not assumed:
- the three real assertions execute and pass with the model present
(13.7s, not an early return)
- forcing `Embedder::new()` to `Err` with the require-flag set panics
the job (checked directly, not inferred from reading the branch)
- without the require-flag, the same forced failure quietly passes
(reproduces the exact pre-fix defect on demand)
- nextest's own `--test <name>` filter already exits nonzero
("error: no tests to run") if the feature ever gets dropped from this
job and the file compiles to zero tests -- confirmed locally, so no
redundant grep-based guard was added on top of it
Refs: PROB-102, PRD-086
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es (#473) Bumps the rust-deps group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` | | [thiserror](https://github.com/dtolnay/thiserror) | `2.0.19` | `2.0.20` | | [futures](https://github.com/rust-lang/futures-rs) | `0.3.32` | `0.3.34` | | [async-trait](https://github.com/dtolnay/async-trait) | `0.1.91` | `0.1.92` | | [globset](https://github.com/BurntSushi/ripgrep) | `0.4.19` | `0.4.20` | | [tokenizers](https://github.com/huggingface/tokenizers) | `0.23.1` | `0.23.2` | | [libc](https://github.com/rust-lang/libc) | `0.2.187` | `0.2.189` | | [clap](https://github.com/clap-rs/clap) | `4.6.3` | `4.6.6` | | [cliclack](https://github.com/fadeevab/cliclack) | `0.5.5` | `0.5.6` | Updates `serde` from 1.0.228 to 1.0.229 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](serde-rs/serde@v1.0.228...v1.0.229) Updates `thiserror` from 2.0.19 to 2.0.20 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](dtolnay/thiserror@2.0.19...2.0.20) Updates `futures` from 0.3.32 to 0.3.34 - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/main/CHANGELOG.md) - [Commits](rust-lang/futures-rs@0.3.32...0.3.34) Updates `async-trait` from 0.1.91 to 0.1.92 - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](dtolnay/async-trait@0.1.91...0.1.92) Updates `globset` from 0.4.19 to 0.4.20 - [Release notes](https://github.com/BurntSushi/ripgrep/releases) - [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md) - [Commits](BurntSushi/ripgrep@globset-0.4.19...globset-0.4.20) Updates `tokenizers` from 0.23.1 to 0.23.2 - [Release notes](https://github.com/huggingface/tokenizers/releases) - [Changelog](https://github.com/huggingface/tokenizers/blob/main/RELEASE.md) - [Commits](huggingface/tokenizers@v0.23.1...v0.23.2) Updates `libc` from 0.2.187 to 0.2.189 - [Release notes](https://github.com/rust-lang/libc/releases) - [Changelog](https://github.com/rust-lang/libc/blob/0.2.189/CHANGELOG.md) - [Commits](rust-lang/libc@0.2.187...0.2.189) Updates `clap` from 4.6.3 to 4.6.6 - [Release notes](https://github.com/clap-rs/clap/releases) - [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md) - [Commits](clap-rs/clap@clap_complete-v4.6.3...clap_complete-v4.6.6) Updates `cliclack` from 0.5.5 to 0.5.6 - [Commits](https://github.com/fadeevab/cliclack/commits) --- updated-dependencies: - dependency-name: serde dependency-version: 1.0.229 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: thiserror dependency-version: 2.0.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: futures dependency-version: 0.3.34 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: async-trait dependency-version: 0.1.92 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: globset dependency-version: 0.4.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: tokenizers dependency-version: 0.23.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: libc dependency-version: 0.2.189 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: clap dependency-version: 4.6.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps - dependency-name: cliclack dependency-version: 0.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…updates (#459) Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `7.0.1` | | [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) | `2.9.1` | `2.9.2` | | [actions/setup-node](https://github.com/actions/setup-node) | `6.0.0` | `7.0.0` | | [mozilla-actions/sccache-action](https://github.com/mozilla-actions/sccache-action) | `0.0.10` | `0.0.11` | | [taiki-e/install-action](https://github.com/taiki-e/install-action) | `2.85.1` | `2.87.5` | Updates `actions/checkout` from 6.0.2 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v6.0.2...3d3c42e) Updates `Swatinem/rust-cache` from 2.9.1 to 2.9.2 - [Release notes](https://github.com/swatinem/rust-cache/releases) - [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md) - [Commits](Swatinem/rust-cache@c193711...6323deb) Updates `actions/setup-node` from 6.0.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@2028fbc...8207627) Updates `mozilla-actions/sccache-action` from 0.0.10 to 0.0.11 - [Release notes](https://github.com/mozilla-actions/sccache-action/releases) - [Commits](Mozilla-Actions/sccache-action@9e7fa8a...fc920bf) Updates `taiki-e/install-action` from 2.85.1 to 2.87.5 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](taiki-e/install-action@3d7d7cd...5bf6ce0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: mozilla-actions/sccache-action dependency-version: 0.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: Swatinem/rust-cache dependency-version: 2.9.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.87.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ad race Found on this job's own first run against `dev`: `dimension_is_unchanged` and `embeddings_match_the_captured_reference` both call `Embedder::new()`, and nextest's default concurrency ran them at the same time. Against a genuinely empty cache both started fetching the same ~2.1 GB model into the same directory at once -- one finished in 62s, the other failed 7s in trying to fetch `onnx/model.onnx_data`. Once `actions/cache` has populated the directory this cannot recur -- `find_snapshot` returns immediately, no write happens -- but the first run after any cache eviction hits it every time. That run reporting red for a race rather than a real defect is exactly the shape that trains people to re-run and ignore rather than investigate, which is the opposite of what this job exists for. `--test-threads=1` removes the only concurrent-download path in this 2-test file; parallelism buys nothing here anyway. Refs: PROB-102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ches-file fix(trust): the lifecycle reason and the embedding oracle both reported success while doing nothing
Trust the number, the write, and the gate that checks it. Three defects that all reported success while doing nothing: an EvidencePack with no verdict/congruence_level scoring 1.00, forgeplan deprecate/renew/reopen writing their reason nowhere the file could keep it, and the embedding correctness oracle never once running in CI since it was written. - CHANGELOG: renamed Unreleased -> 0.37.0, added the sprint headline, a scope note, the #478 and PROB-102 Fixed entries, and a Security section pointing at today's dependabot triage. - Version bumped to 0.37.0 in all 5 pin locations (workspace.package + 4 path-dependency pins across forgeplan-cli/forgeplan-mcp). - CLAUDE.md Current status entry (English per author request) and the test-count line (3290 -> 3331, measured fresh on this branch). - README badges and dogfood table refreshed: 394 -> 437 artifacts, 3290 -> 3331 tests. - docs/operations/dependabot-triage-2026-09-08.md: 33 open (1 rust LOW lru, accepted-with-justification, carried since v0.33.0; 32 npm website, scheduled), cargo-deny confirmed green on dev directly rather than inferred from Dependabot's silence (RustSec isn't mirrored there and that gap cost two prior releases a red gate). Verified fresh on this branch: cargo fmt clean, MCP tool count 73 no drift, smoke-test.sh green, 3331 tests passed across all three crates (one known #454 git-flake and one resource-contention timeout, both confirmed non-regressions by isolated rerun). Refs: PRD-086, PROB-102, PROB-105, #478, #472, #481 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts v0.37.0 off
dev(32 commits since v0.36.0). Merge commit intomain, per convention — do not squash.Sprint headline
Trust the number, the write, and the gate that checks it. Three defects that all reported success while doing nothing:
verdict/congruence_levelscored 1.00 — the opposite of every document describing this system (CLAUDE.mdRED LINE Release v0.2.0 — Phase 3 complete #7,EVIDENCE-PROTOCOL.md).forgeplan deprecate/renew/reopenprinted a reason and put it nowhere durable — the file got the status, the reason went only to LanceDB (gitignored), and the next lifecycle command erased it from there too.embedding_reference.rs) had run in CI zero times since it was written — entirely behindsemantic-search, compiled bycheck/clippy, never executed by the plainnextest run.None of these looked broken. Each reported success in the right format, right up until someone read the file it claimed to have written to.
Breaking
R_eff for any artifact whose weakest evidence pack lacks
verdict/congruence_leveldrops from 1.0 to 0.1. Runforgeplan score --allafter upgrading.What's in it
Full detail in
CHANGELOG.mdunder[0.37.0]. Summary:informstrust flow, Note-poisons-cascade (R_eff: weakest-link cascade zeroes well-evidenced artifacts (ancestor zero overrides local evidence) #392, narrowed), phase regression (Phase machine: forgeplan_validate regresses phase fromdone→validatefor already-activated artifacts #330), anomaly-detector self-checks (anomaly detector: below-threshold R_eff shown as "=0", depth-cap mislabelled as "cycle or depth cap", walk shallower than forgeplan_score #393),embedloading a model for nothing (PROB-103), SPEC validator inversion (validate: нет правила «каждый Requirement в SPEC имеет Scenario» — инвариант держит только TDD-потребитель #450/PROB-105), lifecycle reason data-loss (lifecycle: deprecate/reopen write the reason to LanceDB only — it never reaches the markdown file, and lance/ is gitignored #478), embedding-oracle CI gap (PROB-102, plus a cold-cache download race found and fixed on the same PR).spec-requirement-has-scenario,prd-nfr-exist/prd-nfr-measurablevalidator rules.lru, accepted-with-justification, carried since v0.33.0; 32 npm website, scheduled).cargo-denyconfirmed green ondevdirectly. Full triage:docs/operations/dependabot-triage-2026-09-08.md.Website buildgate (npm-website group (#442, astro 6→7 inside): opened before the Website build gate existed #485).Verification
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningsscripts/check-mcp-tool-count.shbash scripts/smoke-test.shcargo testacross all 3 cratesgit::testsflake (#454) and one resource-contention timeout under heavy parallel local load, both confirmed non-regressions by isolated rerunforgeplan embedmust show semantic-search actually linked, not just compiled)Not in this release