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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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: FeatureView gets a provenance field with the repository, its fork parent (when it's a fork) and who last changed forge.json, with the date. It is filled in listFeatures and getFeature in src/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 one git log per app only when the tip moves.
  • Display: provenanceLine in src/web/pages/features.ts renders 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_plan tool in this run.

To try it, sign in as a site admin and open /settings/features.

Closes #9770

commented

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: FeatureView gets a provenance field with the repository, its fork parent (when it's a fork) and who last changed forge.json, with the date. It is filled in listFeatures and getFeature in src/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 one git log per app only when the tip moves.
  • Display: provenanceLine in src/web/pages/features.ts renders 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_plan tool in this run.

To try it, sign in as a site admin and open /settings/features.

commented

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 — before the latest push

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.

  1. 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.
  2. 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 a dl row (principle 7), not a span with <br /> and a · separator.
  3. Principle 1: the title already prints f.repo in a mono span, and the new line then says 'From <repo>' again. Drop the repeat and show only the upstream and last-change facts.
  4. Principle 8: 'declaration last changed by' is jargon. Say 'Last changed by Ada on 2026-11-01', or similar.
  5. The pull title says the row; the detail page /settings/features/:slug still shows nothing, so provenance is null on write replies. Either show it there or say why not.
  6. The fork path is only unit-tested on provenanceLine. Add one test that builds a real fork and checks upstream through getFeature.
  7. 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.

commented

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-rows dl rows, which are already registered, so there is no new class. docs/design/areas/features.md says so.
  • Both doors: one shared provenanceFacts() in src/actions/features.ts feeds 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/:slug shows the same rows, because getFeature already fills provenance. It is null only in the shared base view that write replies build, and those replies re-read through getFeature before they draw.
  • Cache: the last-change cache has its own bound, CHANGED_MAX = 500, and no longer borrows DECLARED_MAX. The lookup is has then get. Each tip costs one git log, then it is cached.
  • Docs: docs/features/absorb.md has 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 its upstream through getFeature, 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 dl sits inside the row's span.row-meta, which is loose HTML. I kept it because row() has no slot for a block of facts.
Changes requested — before the latest push

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.

  1. Security (high): getFeature is open to anyone who can read the app, and provenanceOf prints parent.ownerName/parent.name with no canRead check 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.
  2. 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 log per app to every list render. Hold it until the pipeline issues land, or name the pipeline issue it serves.
  3. Design: the dl sits inside span.row-meta, a block element in an inline span. Give row() a slot for a block of facts, or render the facts outside the span, instead of leaving loose HTML.
  4. Tests: the assertions not.toContain('From <span') and not.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 the changed cache by tip does not rerun git log for the same tip.
  5. 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.
  6. Cleanliness: provenance: FeatureView | null plus the {...viewOf(), provenance: await ...} spread repeated in two places. Pass provenance into viewOf so there is one path. The very long lines in listFeatures and provenanceFacts need wrapping to match the file.
commented

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() in src/web/ui.ts: a new facts option draws a dl.row-facts under the meta. When facts are present, the row's main column is a <div>, so the dl is valid HTML. Fold rows are left out because a <summary> takes only inline content. The new class is registered in docs/design/areas/lists.md.
  • Feature page: it uses the existing .status-rows dl rows.
  • One path for the view: viewOf takes the provenance as an argument, so there is no more spread over its result, and FeatureView.provenance is 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 one git log per 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-facts markup on the list, and the "Last changed by" row on the feature page.
  • Facts are plain text and escaped.
  • git log runs 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

commented

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() in src/web/ui.ts: a new facts option draws a dl.row-facts under the meta. The row's main column becomes a <div> only when facts are present, so the dl is valid HTML; the option is ignored on fold rows. A row without facts renders byte for byte as before. The new class is registered in docs/design/areas/lists.md. A feature's own page uses the existing .status-rows.
  • src/actions/features.ts: provenanceOf(app, actor, repo) checks canRead on the parent before naming it. Every FeatureView goes through viewOf with its provenance, so it is never null.
  • Cache: lastChange keeps the last change per commit, so a list render runs git log for an app only when its default branch has moved. It has its own size bound.
  • Docs: docs/features/absorb.md explains 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 log runs 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

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:

  1. 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.
  2. 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.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs