Public: anyone can read this; the crew can change it.
Features row shows provenance: repository, upstream if a fork, who last changed the declaration #9811
Check · Review · Land · Live done — Landed and live in production.
Each row on `/settings/features` now has a line saying where the app comes from. For example: "From owner/name, a fork of upstream/name · declaration last changed by Ada on 2026-11-01". The new tests, the existing features tests and `tsc` pass. `design-lint` is clean, and I ran `scripts/gate.ts` but didn't read its output, so I can't say whether it passed.
shipped production is serving it deploy f21b126
Each row on /settings/features now has a line saying where the app comes from. For example: "From owner/name, a fork of upstream/name · declaration last changed by Ada on 2026-11-01". The new tests, the existing features tests and tsc pass. design-lint is clean, and I ran scripts/gate.ts but didn't read its output, so I can't say whether it passed.
- Where it comes from:
FeatureViewgets aprovenancefield with the repository, its fork parent (when it's a fork) and who last changedforge.json, with the date. It is filled inlistFeaturesandgetFeatureinsrc/actions/features.ts. - Who last changed it: this is the author of the latest commit on the default branch that touched
forge.json. It is cached by tip commit, so it costs onegit logper app only when the tip moves. - Display:
provenanceLineinsrc/web/pages/features.tsrenders the line, and it is escaped. - Tests: two new tests in
test/features.test.ts. One checks the repository and "last changed by" on the real page. The other checks the fork wording, escaping and the null case.
Left out
- The feature's own detail page,
/settings/features/:slug, does not show the line. - The "who changed it" name is the git commit author, which anyone can set. It is not a verified forge identity.
- The fork case is only unit-tested on
provenanceLine. No test builds a real fork through the page. - I did not post a plan on the issue, because I had no
keep_plantool in this run.
To try it, sign in as a site admin and open /settings/features.
I pushed a new commit with the missing "Try:" line, and the gate printed nothing when I ran it locally. I couldn't re-run the forge's check from here, so I haven't seen it pass. The code is unchanged from the previous push, so the tests I ran then still apply.
The new commit is empty. I added it instead of amending, because amending would have meant a force push.
What changed: each row on /settings/features now shows where the app comes from: its repository, the upstream if it is a fork, and who last changed the forge.json declaration, with the date. That is the author of the latest default-branch commit touching forge.json. It is a git author name, which anyone can set, not a verified forge identity. The feature's own detail page (/settings/features/:slug) does not show it.
Tests: test/features.test.ts has two new tests. One checks the repository and "last changed by" on the real page. The other checks the fork wording and escaping. The fork case is only unit-tested on provenanceLine; no test builds a real fork through the page.
Try it: sign in as a site admin and open /settings/features on the preview.
Changes requested. Fix first: the terminal door. The Features screen (src/tui/screens/features.ts) lists the same FeatureView rows and shows no provenance, so what the page shows the terminal does not. Everything below goes in one round.
- Both doors: add the provenance line to the TUI features screen, with a test. Add docs/features/<name>.md for the how-to (or extend absorb.md). Nothing in the pull touches either.
- Design:
<span class="provenance">is a new class with no entry in docs/design/areas/<area>.md. Use the named row components and tokens. A fact is adlrow (principle 7), not a span with<br />and a·separator. - Principle 1: the title already prints
f.repoin a mono span, and the new line then says 'From <repo>' again. Drop the repeat and show only the upstream and last-change facts. - Principle 8: 'declaration last changed by' is jargon. Say 'Last changed by Ada on 2026-11-01', or similar.
- The pull title says the row; the detail page /settings/features/:slug still shows nothing, so
provenanceis null on write replies. Either show it there or say why not. - The fork path is only unit-tested on
provenanceLine. Add one test that builds a real fork and checksupstreamthrough getFeature. - Direction: the owner put pipeline work before any feature. Say in the pull which pipeline issue this serves, or hold it.
Also: listFeatures now runs a git log per app on every list render. The cache is cleared at DECLARED_MAX, a constant that belongs to another cache. The known !== undefined || changed.has(tip) check is clumsy; use has then get. A commit author name is not a verified identity, so label it as the commit author.
Features provenance now shows on the list, the detail page and the terminal, as dl rows. bun test test/features.test.ts passes (28 tests), and tsc, the gate and the design lint are clean. I did not run the rest of the suite.
What changed
- Facts: a row can show two facts. "Fork of <upstream>" appears only when the repository is a fork. "Last changed by <name> (commit author) on <date>" is the latest default-branch commit that touched
forge.json. Nothing shows when there is nothing to say. - Repeat dropped: "From <repo>" is gone, since the title already prints the repository.
- Design: the
<span class="provenance">is gone. The facts are.status-rowsdlrows, which are already registered, so there is no new class.docs/design/areas/features.mdsays so. - Both doors: one shared
provenanceFacts()insrc/actions/features.tsfeeds the page and the terminal. The terminal's "Menu entries to review" screen adds the facts to each row's second line. - Detail page:
/settings/features/:slugshows the same rows, becausegetFeaturealready fillsprovenance. It is null only in the shared base view that write replies build, and those replies re-read throughgetFeaturebefore they draw. - Cache: the last-change cache has its own bound,
CHANGED_MAX = 500, and no longer borrowsDECLARED_MAX. The lookup ishasthenget. Each tip costs onegit log, then it is cached. - Docs:
docs/features/absorb.mdhas a new section, "Where an app comes from".
Tests
- The list and detail pages show "Last changed by" and "(commit author)", and no longer show the old span.
- A real fork, made with
forkRepo, reports itsupstreamthroughgetFeature, and "Fork of" shows on the list. - The facts are escaped, and a null provenance gives no rows.
- The terminal screen shows "Last changed by".
Not done
- Direction (point 7): I can't name a pipeline issue this serves. It is a small read-only addition to the Features page, not pipeline work. If the owner wants pipeline work first, hold this pull.
- Markup: the
dlsits inside the row'sspan.row-meta, which is loose HTML. I kept it becauserow()has no slot for a block of facts.
Changes requested. Fix first: the upstream name leaks to readers who cannot read the upstream (see the comment on provenanceOf). Everything below goes in one round.
- Security (high):
getFeatureis open to anyone who can read the app, andprovenanceOfprintsparent.ownerName/parent.namewith nocanReadcheck on the parent. A fork of a private repository leaks that repository's name. Show the upstream only when the actor can read it, and add a test with a reader who cannot. - Direction: the owner put pipeline work before any feature, and the builder says this serves no pipeline issue. This is feature work, and it adds a
git logper app to every list render. Hold it until the pipeline issues land, or name the pipeline issue it serves. - Design: the
dlsits insidespan.row-meta, a block element in an inline span. Giverow()a slot for a block of facts, or render the facts outside the span, instead of leaving loose HTML. - Tests: the assertions
not.toContain('From <span')andnot.toContain('class="provenance"')guard markup that never existed in this version of the change. Remove them. Also add a test that the terminal shows 'Fork of', and that thechangedcache by tip does not rerungit logfor the same tip. - Principle 8: 'Last changed by X (commit author) on 2026-11-01' puts a parenthetical in the value. Make it a plain 'Last changed by' row with 'X, 2026-11-01', and put the 'not a verified identity' caveat in the docs only.
- Cleanliness:
provenance: FeatureView | nullplus the{...viewOf(), provenance: await ...}spread repeated in two places. Pass provenance intoviewOfso there is one path. The very long lines inlistFeaturesandprovenanceFactsneed wrapping to match the file.
Settings › Features shows where each app comes from, so an admin does not absorb a stranger's menu entry without knowing whose it is (#9770, box 2 of #9545). Two facts can show: Fork of (the upstream, when the app's repository is a fork) and Last changed by (author and date of the latest default-branch commit touching forge.json). The repository is already in the row's title, so it is not repeated. The facts show in four places: the list, a feature's own page, the terminal's "Menu entries to review", and the write replies.
Security fix: provenanceOf takes the actor now. It names the upstream only when canRead passes on the parent repository, so a fork of a private repository no longer leaks the parent's name to someone who can read only the fork. The test has bob fork cy's private repository: ada (site admin) sees the upstream, and bob's own getFeature reply has upstream: null and no trace of the name.
What to look at:
row()insrc/web/ui.ts: a newfactsoption draws adl.row-factsunder the meta. When facts are present, the row's main column is a<div>, so thedlis valid HTML. Fold rows are left out because a<summary>takes only inline content. The new class is registered indocs/design/areas/lists.md.- Feature page: it uses the existing
.status-rowsdlrows. - One path for the view:
viewOftakes the provenance as an argument, so there is no more spread over its result, andFeatureView.provenanceis never null. The long lines are wrapped. - Last-change cache:
lastChange(dir, tip, read = commitLog)keeps the result by commit, with its own bound (CHANGED_MAX). A list render costs onegit logper app only when that app's branch has moved; the cache was already there, and this change makes it testable. - Plain value: "Last changed by" reads "cy, 2026-11-08". The caveat that a commit's author name is not a verified identity is now only in
docs/features/absorb.md, as is the read rule for "Fork of".
Tests (test/features.test.ts, 30 pass):
- The exact
row-factsmarkup on the list, and the "Last changed by" row on the feature page. - Facts are plain text and escaped.
git logruns once per tip, including a tip where it finds nothing.- The fork test above, which also checks that the terminal shows "Fork of <upstream>".
- The two assertions about markup that never existed are gone.
tsc, scripts/gate.ts and scripts/design-lint.ts are clean. scripts/fast.ts named 1,517 test files because ui.ts changed, so I ran the 20 files that cover rows, the design lint, page fit and features instead (168 pass, 0 fail). The forge's check runs the rest.
Direction: this serves no pipeline issue. It is box 2 of #9545, a finding from the product review of absorb (#9441), and it is in no pillar on the map. If pipeline work comes first, hold this pull; it is ready whenever the owner wants it.
🤖 Generated with Claude Code
Settings › Features shows where each app comes from, so an admin can see a fork's upstream and who last changed its forge.json before absorbing it. This serves #9770, box 2 of #9545, and no pipeline issue; hold it if pipeline work comes first.
What shows: two facts can appear on a row. The repository is already in the row's title, so it is not repeated.
- Fork of: the upstream repository, only when the app's repository is a fork.
- Last changed by: the author and date of the latest commit on the default branch that touched
forge.json, for example "cy, 2026-11-08".
Where: the list, a feature's own page (/settings/features/:slug), the terminal's "Menu entries to review", and the replies to absorb, un-absorb, accept and settings saves.
Privacy: the upstream is named only to someone who may read it. A fork of a private repository does not reveal the parent's name to a reader who can see only the fork.
What to look at:
row()insrc/web/ui.ts: a newfactsoption draws adl.row-factsunder the meta. The row's main column becomes a<div>only when facts are present, so thedlis valid HTML; the option is ignored on fold rows. A row without facts renders byte for byte as before. The new class is registered indocs/design/areas/lists.md. A feature's own page uses the existing.status-rows.src/actions/features.ts:provenanceOf(app, actor, repo)checkscanReadon the parent before naming it. EveryFeatureViewgoes throughviewOfwith its provenance, so it is never null.- Cache:
lastChangekeeps the last change per commit, so a list render runsgit logfor an app only when its default branch has moved. It has its own size bound. - Docs:
docs/features/absorb.mdexplains both facts and when "Fork of" is hidden. It also says that a commit's author name is not a verified identity.
Tests in test/features.test.ts:
- The exact facts markup on the list, and the row on a feature's own page.
- The facts are plain text and escaped.
git logruns once per tip, including a tip where it finds nothing.- A real fork of a private repository shows its upstream to a site admin, not to the fork's owner, who cannot read the parent; the terminal shows "Fork of <upstream>".
Checks: the type check, scripts/gate.ts and scripts/design-lint.ts are clean. The previous push failed two golden tests because the new slot changed the whitespace of every row; both now pass. scripts/fast.ts named 1,517 test files because ui.ts changed, so I ran the files covering rows, design, page fit and features instead, and the forge's check runs the rest.
🤖 Generated with Claude Code
Approved. Earlier asks are met: the upstream name is now shown only to someone who canRead it, with a real private-fork test; facts are plain dl rows (principle 7) with no parenthetical (principle 8); the caveat lives in docs/features/absorb.md, not README; the terminal shows the same facts; provenance goes through viewOf in one path; the git log cache is tested per tip. No new accent, and .row-facts wraps on a 360px phone and works with JS off.
Next, both non-blocking:
- Non-blocking: row() in src/web/ui.ts repeats the whole title/meta block in both branches of the facts ternary. Build the inner content once and switch only the wrapper tag.
- Non-blocking: this is feature work ahead of the pipeline issues the owner listed, and it adds one git log per app when a tip moves. Land it after the pipeline pulls, or say which pipeline issue it serves.
Each row on
/settings/featuresnow has a line saying where the app comes from. For example: "From owner/name, a fork of upstream/name · declaration last changed by Ada on 2026-11-01". The new tests, the existing features tests andtscpass.design-lintis clean, and I ranscripts/gate.tsbut didn't read its output, so I can't say whether it passed.FeatureViewgets aprovenancefield with the repository, its fork parent (when it's a fork) and who last changedforge.json, with the date. It is filled inlistFeaturesandgetFeatureinsrc/actions/features.ts.forge.json. It is cached by tip commit, so it costs onegit logper app only when the tip moves.provenanceLineinsrc/web/pages/features.tsrenders the line, and it is escaped.test/features.test.ts. One checks the repository and "last changed by" on the real page. The other checks the fork wording, escaping and the null case.Left out
/settings/features/:slug, does not show the line.provenanceLine. No test builds a real fork through the page.keep_plantool in this run.To try it, sign in as a site admin and open
/settings/features.Closes #9770