v0.42.31.0 feat(links): open link_source provenance + link-add/link-rm/link-sources (#1941) (#1957)

* feat(links): relax link_source CHECK to kebab-case provenance + migration v114

Open link_source from a closed allowlist to a kebab-case format gate
(^[a-z][a-z0-9]*(-[a-z0-9]+)*$, char_length<=64) so external derivers
stamp their own provenance (e.g. citation-graph) without a per-deriver
migration. Migration v114: Postgres NOT VALID + VALIDATE (lock-friendly,
transaction:false); PGLite plain DROP+ADD. Updates the schema.sql +
engine provenance contract comments. (#1941)

* feat(links): expose link provenance on link ops + link-add/link-rm/link-sources

add_link/remove_link now accept --link-source/--link-type; add_link guards
the reconciliation-managed built-ins (markdown/frontmatter/mentions/
wikilink-resolved) and defaults omitted provenance to 'manual' (was the
misleading engine default 'markdown'). New cliHints.aliases mechanism with a
startup collision guard registers link-add/link-rm; printOpHelp shows the
invoked alias name. New list_link_sources read op + listLinkSources engine
method (both engines, {sourceId?,sourceIds?}, deterministic order) powers
`gbrain link-sources`, added to the minion read allowlist. (#1941)

* test(links): kebab provenance, op guard, link-sources, aliases + parity

Covers the v114 regex/length boundaries, upgrade-path constraint swap on
existing data, the managed-built-in op guard + manual default, remove_link
type/source filters, list_link_sources scoping (scalar + federated) and
PG/PGLite parity, and alias resolution/collision/help. Fixes the prior
'inferred'-rejection assertion (now valid kebab) in the mentions test. (#1941)

* chore: bump version and changelog (v0.42.31.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: update KEY_FILES for v0.42.31.0 link provenance surface

KEY_FILES.md current-state updates for #1941: link_source now an open
kebab-case provenance (migration v114), the add_link/remove_link guard +
defaults, list_link_sources + listLinkSources, and cliHints.aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(minions): supervisor queue-singleton keying + pidfile cleanup (follow-up #1849)

Two correctness bugs in the v0.42.29.0 supervisor-singleton work, caught by
adversarial review:

- supervisorLockId mixed a config-derived DB identity into the key, but the
  lock row already lives inside the target database. Two supervisors on the
  same physical DB via different-but-equivalent URLs (pooler vs direct port,
  host alias, trailing params) computed different ids and BOTH acquired the
  "singleton" lock. Key on the queue alone; the database half of the mutex is
  physical. Removes the now-dead currentDbIdentity() from worker-registry.

- The pidfile-cleanup process.on('exit') listener was installed AFTER the
  DB-lock acquire, so the LOCK_HELD early-exit stranded the pidfile this
  process had just created. Install the listener first.

Regression test pins the listener-before-lock ordering; updates the lockId
test to the queue-only invariant; KEY_FILES updated to current state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-07 19:12:50 -07:00
committed by GitHub
parent f8d4ce6fc4
commit f401d7407e
21 changed files with 716 additions and 106 deletions

View File

@@ -2663,6 +2663,30 @@ export class PostgresEngine implements BrainEngine {
return rows as unknown as Link[];
}
async listLinkSources(
opts?: { sourceId?: string; sourceIds?: string[] },
): Promise<{ link_source: string | null; count: number }[]> {
const sql = this.sql;
// v114 (#1941): distinct provenances + counts for `gbrain link-sources`.
// Scope by the FROM page's source (consistent with getLinks). Federated
// {sourceIds} takes precedence over scalar {sourceId}; neither = unscoped.
const sourceCondition =
opts?.sourceIds && opts.sourceIds.length > 0
? sql`WHERE f.source_id = ANY(${opts.sourceIds}::text[])`
: opts?.sourceId
? sql`WHERE f.source_id = ${opts.sourceId}`
: sql``;
const rows = await sql`
SELECT l.link_source, COUNT(*)::int AS count
FROM links l
JOIN pages f ON f.id = l.from_page_id
${sourceCondition}
GROUP BY l.link_source
ORDER BY count DESC, l.link_source ASC NULLS LAST
`;
return rows as unknown as { link_source: string | null; count: number }[];
}
async findByTitleFuzzy(
name: string,
dirPrefix?: string,