From 3e6b76bcb2048aa2989095635aeb4dfdc95b85f7 Mon Sep 17 00:00:00 2001 From: Yaacov Date: Sat, 18 Jul 2026 16:00:32 +0300 Subject: [PATCH] docs: align product and owned source truth --- .github/workflows/gate-1-5-evidence.yml | 12 +- docs/operations/upstream-intake.md | 3 +- docs/product/PRD.md | 17 ++- lab/external/README.md | 5 + lab/external/owned-sources.json | 31 +++++ lab/external/sources.lock.md | 54 ++++++-- lab/scripts/verify-owned-sources.mjs | 161 ++++++++++++++++++++++ lab/scripts/verify-owned-sources.test.mjs | 152 ++++++++++++++++++++ 8 files changed, 420 insertions(+), 15 deletions(-) create mode 100644 lab/external/owned-sources.json create mode 100644 lab/scripts/verify-owned-sources.mjs create mode 100644 lab/scripts/verify-owned-sources.test.mjs diff --git a/.github/workflows/gate-1-5-evidence.yml b/.github/workflows/gate-1-5-evidence.yml index 48a80ab..4435d25 100644 --- a/.github/workflows/gate-1-5-evidence.yml +++ b/.github/workflows/gate-1-5-evidence.yml @@ -1,4 +1,4 @@ -name: Gate 1.5 evidence +name: Scient evidence on: pull_request: @@ -13,9 +13,13 @@ jobs: evidence-manifest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 22 + node-version: 24 - name: Verify committed Gate 1.5 evidence run: node lab/scripts/verify-gate-1-5-manifest.mjs + - name: Test owned-source evidence verifier + run: node --test lab/scripts/verify-owned-sources.test.mjs + - name: Verify current owned-source evidence + run: node lab/scripts/verify-owned-sources.mjs diff --git a/docs/operations/upstream-intake.md b/docs/operations/upstream-intake.md index 162f30f..9616ec5 100644 --- a/docs/operations/upstream-intake.md +++ b/docs/operations/upstream-intake.md @@ -161,7 +161,8 @@ branch. 5. Create a dated record from `lab/external/upstream-reviews/review-template.md`. 6. Update the repo-local state to the exact reviewed tip and run - `--review-check`. + `--require-reviewed-tip`. The older `--review-check` spelling remains a + strict compatibility alias. 7. Submit the source-state and parent-evidence changes through their own repository PRs. Cross-link dependencies; do not combine repositories into one Git commit. diff --git a/docs/product/PRD.md b/docs/product/PRD.md index d8d830e..e9936e7 100644 --- a/docs/product/PRD.md +++ b/docs/product/PRD.md @@ -4,7 +4,7 @@ Status: Accepted Version: v1 Owner: Yaacov Created: 2026-06-27 -Last updated: 2026-07-17 +Last updated: 2026-07-18 Purpose: Defines Scient's product direction, core capabilities, user experience principles, and product constraints. Doc type: Product truth @@ -18,7 +18,13 @@ identity; the native Scient agent remains planned. Implementation plans, package structure, task sequencing, and framework-specific code patterns do not belong in the PRD. Those belong in planning, architecture, development, or quality docs. -Open-source adaptation strategy and implementation source choices live outside the PRD. Current source research belongs in `docs/research/source-evaluations/open-source-adaptation-map.md`; build sequencing and fork/adapter strategy belong in `docs/planning/open-source-adaptation-build-strategy.md` until promoted into architecture documents or ADRs. +Open-source adaptation strategy and implementation source choices live outside +the PRD. Current source research belongs in +`docs/research/source-evaluations/open-source-adaptation-map.md`; build +sequencing lives in +`docs/planning/open-source-adaptation-build-strategy.md`; and the accepted +standalone ownership and selective-upstream policy lives in +`docs/architecture/decisions/ADR-0002-standalone-source-ownership-and-upstream-authority.md`. ### Update Policy @@ -40,6 +46,13 @@ implemented-versus-planned boundary live in `scient-product-identity.md`; the completed migration and compatibility record lives in `../planning/papilab-to-scient-rename-execution-plan.md`. +Repository names are technical ownership labels, not additional product +brands: `ScientFactory/Scient` owns product truth and the scientific boundary, +`ScientFactory/scient-desktop` owns the implemented desktop application, and +`ScientFactory/scient-agent` is the maintained source foundation for the +planned native Scient agent. The repository name does not imply that the +native agent runtime is already implemented. + ## Product Overview Scient is a local-first scientific workspace where researchers, collaborators, and AI agents run an entire research project together, from early project formation through publication-ready outputs. Each project brings research materials, sources, data, analysis work, writing, citations, decisions, memory, collaboration, and outputs into one durable workspace. diff --git a/lab/external/README.md b/lab/external/README.md index bed0e5a..fd49a27 100644 --- a/lab/external/README.md +++ b/lab/external/README.md @@ -30,6 +30,9 @@ notes, and lab evidence. maintained owned repositories. - `sources.lock.md` - current local source inventory with URL, branch, commit, role, and update strategy. +- `owned-sources.json` - machine-readable cross-repository source heads and + review checkpoints verified by parent CI against each public source + repository. - `upstream-reviews/` - dated disposition evidence for official source changes; repo-local `upstream-state.json` files remain the machine checkpoints. @@ -40,6 +43,8 @@ notes, and lab evidence. - Record the actual location of any temporary external reference checkout; do not infer a checkout merely because a pointer directory exists. - Record exact source commits in `sources.lock.md`. +- Keep `owned-sources.json`, `sources.lock.md`, and each source repository's + `upstream-state.json` aligned after an owned source head changes. - Prefer one physical checkout per upstream repository. - Use pointer notes when one repository spans multiple roles. - Keep Scient-owned bridge code outside temporary upstream trees unless a fork diff --git a/lab/external/owned-sources.json b/lab/external/owned-sources.json new file mode 100644 index 0000000..6dd00df --- /dev/null +++ b/lab/external/owned-sources.json @@ -0,0 +1,31 @@ +{ + "schema": "scient.owned-sources/v1", + "sources": [ + { + "ownedRepository": "ScientFactory/scient-desktop", + "sourceLockLabel": "Scient desktop", + "ownedDefaultBranch": "main", + "testedHead": "57e6b2cde09f64db367b894506f56db605fb91b4", + "officialRepository": "Emanuele-web04/synara", + "officialDefaultBranch": "main", + "reviewedThrough": "69304bc1d59d86da8afbac367118c75db8c9dbfe", + "reviewedAt": "2026-07-18", + "integrationBase": "9be46c3ce6a7521b64436b7334bc6fce16e3cac4", + "updateMode": "divergent-cherry-pick", + "reviewRecord": "lab/external/upstream-reviews/2026-07-18-scient-desktop.md" + }, + { + "ownedRepository": "ScientFactory/scient-agent", + "sourceLockLabel": "Scient agent source", + "ownedDefaultBranch": "dev", + "testedHead": "f7d61d3583687ddc09919ca9e70d69d06b0861f8", + "officialRepository": "anomalyco/opencode", + "officialDefaultBranch": "dev", + "reviewedThrough": "fab213312927ea64cf968832c527206e8c944f9e", + "reviewedAt": "2026-07-18", + "integrationBase": "69a80663a2ed7d671d2b4d5dd6f2d605714675a5", + "updateMode": "adapter-maintained", + "reviewRecord": "lab/external/upstream-reviews/2026-07-18-scient-agent.md" + } + ] +} diff --git a/lab/external/sources.lock.md b/lab/external/sources.lock.md index 4c1d7ec..62e6951 100644 --- a/lab/external/sources.lock.md +++ b/lab/external/sources.lock.md @@ -26,9 +26,9 @@ siblings. A deferred source with no retained checkout says so explicitly. | Source | Local path | Official upstream | Owned repository | Tested integrated upstream base | Maintained/tested commit | Role and update mode | |---|---|---|---|---|---|---| -| Scient agent source (OpenCode-derived) | `../scient-agent/`; canonical workspace sibling on `dev` at `14003a01350c69dedf90c97f9f2b5db733f49951` | `https://github.com/anomalyco/opencode.git`, `dev` | `https://github.com/ScientFactory/scient-agent`, public standalone repository | `69a80663a2ed7d671d2b4d5dd6f2d605714675a5` | Current owned `dev` `14003a01350c69dedf90c97f9f2b5db733f49951`; exact rename and maintenance evidence below | Owned source foundation for the planned Scient agent; `adapter-maintained`; native Scient runtime identity is not yet implemented. | +| Scient agent source (OpenCode-derived) | `../scient-agent/`; canonical workspace sibling on `dev` at `f7d61d3583687ddc09919ca9e70d69d06b0861f8` | `https://github.com/anomalyco/opencode.git`, `dev` | `https://github.com/ScientFactory/scient-agent`, public standalone repository | `69a80663a2ed7d671d2b4d5dd6f2d605714675a5` | Current owned `dev` `f7d61d3583687ddc09919ca9e70d69d06b0861f8`; exact rename and maintenance evidence below | Owned source foundation for the planned Scient agent; `adapter-maintained`; native Scient runtime identity is not yet implemented. | | Goose | No local checkout is retained. | `https://github.com/aaif-goose/goose.git`, `main` | None; owned repository deferred | Not tested in Gate 1.5 | Last inspected commit `3c1fdd692cc8aaa5f09b9175410c09a09d4dfe49` | Deferred broader-agent research input. Repository, build, ACP adapter, runtime, credentials, and adoption wait until after the first Scient gateway. | -| Scient desktop (Synara-derived) | `../scient-desktop/`; canonical workspace sibling on `main` at `d78388a42bcc09dabc926c0885ec34a8de6427b0` | `https://github.com/Emanuele-web04/synara.git`, `main` | `https://github.com/ScientFactory/scient-desktop`, public standalone repository | `9be46c3ce6a7521b64436b7334bc6fce16e3cac4` | Current owned `main` `d78388a42bcc09dabc926c0885ec34a8de6427b0`; exact rename and maintenance evidence below | Accepted initial application foundation; `divergent-cherry-pick`; must not own scientific project truth. | +| Scient desktop (Synara-derived) | `../scient-desktop/`; canonical workspace sibling on `main` at `57e6b2cde09f64db367b894506f56db605fb91b4` | `https://github.com/Emanuele-web04/synara.git`, `main` | `https://github.com/ScientFactory/scient-desktop`, public standalone repository | `9be46c3ce6a7521b64436b7334bc6fce16e3cac4` | Current owned `main` `57e6b2cde09f64db367b894506f56db605fb91b4`; exact rename and maintenance evidence below | Accepted initial application foundation; `divergent-cherry-pick`; must not own scientific project truth. | | T3 Code | No local checkout is retained. | `https://github.com/pingdotgg/t3code.git`, `main` | None | Not tested in Gate 1.5 | Last inspected commit `b9cc8d6ef17ca9f45bec621bef71ad3f706b9276` | Desktop/runtime/provider/process reference only. | ## Maintained Upstream Review State @@ -40,8 +40,8 @@ review is accepted. | Source | Tested owned head | Reviewed through | Integration base | Update mode | Review evidence | |---|---|---|---|---|---| -| Scient desktop | `d78388a42bcc09dabc926c0885ec34a8de6427b0` | `69304bc1d59d86da8afbac367118c75db8c9dbfe` on 2026-07-18 | `9be46c3ce6a7521b64436b7334bc6fce16e3cac4` | `divergent-cherry-pick` | [`2026-07-18-scient-desktop.md`](upstream-reviews/2026-07-18-scient-desktop.md); no code intake | -| Scient agent source | `14003a01350c69dedf90c97f9f2b5db733f49951` | `fab213312927ea64cf968832c527206e8c944f9e` on 2026-07-18 | `69a80663a2ed7d671d2b4d5dd6f2d605714675a5` | `adapter-maintained` | [`2026-07-18-scient-agent.md`](upstream-reviews/2026-07-18-scient-agent.md); no code intake | +| Scient desktop | `57e6b2cde09f64db367b894506f56db605fb91b4` | `69304bc1d59d86da8afbac367118c75db8c9dbfe` on 2026-07-18 | `9be46c3ce6a7521b64436b7334bc6fce16e3cac4` | `divergent-cherry-pick` | [`2026-07-18-scient-desktop.md`](upstream-reviews/2026-07-18-scient-desktop.md); no code intake | +| Scient agent source | `f7d61d3583687ddc09919ca9e70d69d06b0861f8` | `fab213312927ea64cf968832c527206e8c944f9e` on 2026-07-18 | `69a80663a2ed7d671d2b4d5dd6f2d605714675a5` | `adapter-maintained` | [`2026-07-18-scient-agent.md`](upstream-reviews/2026-07-18-scient-agent.md); no code intake | ## Standalone Ownership And Maintenance Rollout @@ -64,6 +64,28 @@ source-repository pull requests: run `29640292072`, attempt 2, including the browser suite, and merged as `d78388a42bcc09dabc926c0885ec34a8de6427b0`. Final monitor run `29640770607` passed on that exact owned head without opening a review issue. +- Desktop [PR #4](https://github.com/ScientFactory/scient-desktop/pull/4) + established the standalone Scient repository identity and merged as + `0b6f135c2d19e93b0d790b2427c56b6a368a2bca`. +- Desktop [PR #5](https://github.com/ScientFactory/scient-desktop/pull/5) + aligned owned automation with the standalone repository and merged as + `91b38b1c45eb8bdef4da458bbc56d67419269588`. +- Desktop [PR #12](https://github.com/ScientFactory/scient-desktop/pull/12) + established the maintained repository-governance baseline and merged as + `2bb8623f17c28c0dd4d50bf484ac8cf5065ce2eb`. +- Desktop [PR #13](https://github.com/ScientFactory/scient-desktop/pull/13) + hardened immutable workflow and local-action verification; exact head + `4498b72d8edf96bc246088f0b9ca9ba3d516cc33` passed hosted CI run + `29644350698` and merged as + `57e6b2cde09f64db367b894506f56db605fb91b4`. A local arm64 DMG built from + that merge embedded commit `57e6b2cde09f`, had SHA-256 + `41f9abc5a39cfdae470ad5580ea61c7da969023c445d10196db46b4e92df4424`, + and passed the disk-image checksum. A profile-overridden isolated test copy + launched and initialized `PROJECT.md`, `AGENTS.md`, and + `.scient/project.json` in a fresh folder. The artifact is ad-hoc rather than + distribution-signed and fails strict code-sign verification, so public + release remains blocked by + [issue #6](https://github.com/ScientFactory/scient-desktop/issues/6). - Agent [PR #1](https://github.com/ScientFactory/scient-agent/pull/1) established the operator card, review state, verifier modes, owned source quality workflow, and monitor; exact head @@ -81,6 +103,20 @@ source-repository pull requests: source-quality run `29640340180` and merged as `14003a01350c69dedf90c97f9f2b5db733f49951`. Final monitor run `29640673934` passed on that exact owned head without opening a review issue. +- Agent [PR #4](https://github.com/ScientFactory/scient-agent/pull/4) + established the standalone Scient repository identity and merged as + `b2fa83199171b4d9b1bec7287ed7121a9590c38b`. +- Agent [PR #5](https://github.com/ScientFactory/scient-agent/pull/5) + hardened source-review enforcement and inherited workflow isolation; exact + head `3ff0b98634bece0e5b3c295b74d7b877629f48f7` passed hosted source-quality + run `29644353059` and merged as + `a4a9f25eebc5517e139bdd08da3693f389b896e9`. +- Agent [PR #6](https://github.com/ScientFactory/scient-agent/pull/6) + refreshed the exact immutable `nixbuild/nix-quick-install-action` pin in the + retained, disabled Nix workflows; exact head + `012398b080696691864f985f310dfefaf9d9a749` passed hosted source-quality run + `29644889618` and merged as + `f7d61d3583687ddc09919ca9e70d69d06b0861f8`. No source code from the reviewed official ranges was integrated during this rollout. The PRs above establish ownership, review, monitoring, and verification @@ -200,10 +236,12 @@ and [`scient-agent-fork-archive`](https://github.com/ScientFactory/scient-agent-fork-archive). Their official remotes were named `upstream`, retained their official fetch URLs, and used the literal disabled push URL `DISABLED`. The owned default -branches remain protected against direct unreviewed changes, force-push, and -deletion; the desktop repository requires its maintained quality and -release-smoke checks, and the agent-source repository requires -`Scient source quality`. +branches block force-push and deletion and require maintained checks before a +pull request can merge: desktop quality plus release smoke, and +`Scient source quality` for the agent source. The current rules require zero +approving reviews and do not enforce restrictions for administrators; this +document does not overstate those settings as mandatory human approval or +administrator enforcement. After standalone recreation, inherited OpenCode community-management, generated-commit, publication, deployment, scheduled-sync, and closing workflows were disabled; only reviewed Scient quality/read-only checks and the diff --git a/lab/scripts/verify-owned-sources.mjs b/lab/scripts/verify-owned-sources.mjs new file mode 100644 index 0000000..2d47d11 --- /dev/null +++ b/lab/scripts/verify-owned-sources.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SHA = /^[0-9a-f]{40}$/; +const REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; +const MAINTAINED_SOURCES = new Map([ + ["scientfactory/scient-desktop", "main"], + ["scientfactory/scient-agent", "dev"], +]); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const defaultManifestPath = resolve(repoRoot, "lab/external/owned-sources.json"); + +function fail(message) { + throw new Error(`Owned source evidence invalid: ${message}`); +} + +function requiredString(record, field, label) { + const value = record?.[field]; + if (typeof value !== "string" || value.length === 0) fail(`${label}.${field} must be a non-empty string`); + return value; +} + +export function validateOwnedSourcesManifest( + manifest, + root = repoRoot, + maintainedSources = MAINTAINED_SOURCES, +) { + if (manifest?.schema !== "scient.owned-sources/v1") fail("schema must be scient.owned-sources/v1"); + if (!Array.isArray(manifest.sources) || manifest.sources.length === 0) fail("sources must be a non-empty array"); + + const repositories = new Set(); + const sourceLockPath = resolve(root, "lab/external/sources.lock.md"); + if (!existsSync(sourceLockPath)) fail("lab/external/sources.lock.md is missing"); + const sourceLockLines = readFileSync(sourceLockPath, "utf8").split("\n"); + for (const [index, source] of manifest.sources.entries()) { + const label = `sources[${index}]`; + const repository = requiredString(source, "ownedRepository", label); + if (!REPOSITORY.test(repository)) fail(`${label}.ownedRepository is not an owner/repository pair`); + const normalizedRepository = repository.toLowerCase(); + if (repositories.has(normalizedRepository)) fail(`duplicate owned repository: ${repository}`); + const expectedDefaultBranch = maintainedSources.get(normalizedRepository); + if (!expectedDefaultBranch) fail(`unexpected maintained repository: ${repository}`); + repositories.add(normalizedRepository); + + for (const field of ["testedHead", "reviewedThrough", "integrationBase"]) { + if (!SHA.test(requiredString(source, field, label))) fail(`${label}.${field} must be a full commit SHA`); + } + for (const field of [ + "ownedDefaultBranch", + "sourceLockLabel", + "officialRepository", + "officialDefaultBranch", + "reviewedAt", + "updateMode", + "reviewRecord", + ]) requiredString(source, field, label); + if (source.ownedDefaultBranch !== expectedDefaultBranch) { + fail( + `${label}.ownedDefaultBranch expected ${expectedDefaultBranch}, received ${source.ownedDefaultBranch}`, + ); + } + if (!REPOSITORY.test(source.officialRepository)) fail(`${label}.officialRepository is not an owner/repository pair`); + if (!/^\d{4}-\d{2}-\d{2}$/.test(source.reviewedAt)) fail(`${label}.reviewedAt must use YYYY-MM-DD`); + + const reviewPath = resolve(root, source.reviewRecord); + if (!reviewPath.startsWith(`${resolve(root)}/`) || !existsSync(reviewPath)) { + fail(`${label}.reviewRecord is missing or outside the repository: ${source.reviewRecord}`); + } + const review = readFileSync(reviewPath, "utf8"); + if (!review.includes(source.reviewedThrough)) { + fail(`${label}.reviewRecord does not contain reviewedThrough ${source.reviewedThrough}`); + } + const lockRows = sourceLockLines.filter((line) => line.startsWith(`| ${source.sourceLockLabel}`)); + if (lockRows.length < 2 || lockRows.some((line) => !line.includes(source.testedHead))) { + fail(`${label}.testedHead is not aligned across sources.lock.md rows for ${source.sourceLockLabel}`); + } + for (const field of ["reviewedThrough", "reviewedAt", "integrationBase", "updateMode"]) { + if (!lockRows.some((line) => line.includes(source[field]))) { + fail(`${label}.${field} is not aligned across sources.lock.md rows for ${source.sourceLockLabel}`); + } + } + } + const omittedRepositories = [...maintainedSources.keys()].filter( + (repository) => !repositories.has(repository), + ); + if (omittedRepositories.length > 0) { + fail(`maintained repositories omitted from manifest: ${omittedRepositories.join(", ")}`); + } + return manifest.sources; +} + +// The maintained sources are public. Keep cross-repository requests +// unauthenticated because the parent repository's scoped Actions token is not +// granted to those repositories and can turn public requests into 404s. +async function fetchJson(url, fetchImpl) { + const response = await fetchImpl(url, { + headers: { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + if (!response.ok) fail(`request failed (${response.status}) for ${url}`); + return response.json(); +} + +export async function verifyOwnedSources(manifest, options = {}) { + const root = options.root ?? repoRoot; + const fetchImpl = options.fetchImpl ?? fetch; + const sources = validateOwnedSourcesManifest(manifest, root, options.maintainedSources); + + for (const source of sources) { + const rawUrl = `https://raw.githubusercontent.com/${source.ownedRepository}/${source.testedHead}/upstream-state.json`; + const state = await fetchJson(rawUrl, fetchImpl); + const expectedState = { + ownedRepository: source.ownedRepository, + ownedDefaultBranch: source.ownedDefaultBranch, + officialRepository: source.officialRepository, + officialDefaultBranch: source.officialDefaultBranch, + updateMode: source.updateMode, + reviewedThrough: source.reviewedThrough, + reviewedAt: source.reviewedAt, + integrationBase: source.integrationBase, + reviewRecord: `ScientFactory/Scient:${source.reviewRecord}`, + }; + for (const [field, expected] of Object.entries(expectedState)) { + if (state?.[field] !== expected) { + fail(`${source.ownedRepository} upstream-state.json ${field} expected ${expected}, received ${state?.[field]}`); + } + } + + const repositoryUrl = `https://api.github.com/repos/${source.ownedRepository}`; + const repository = await fetchJson(repositoryUrl, fetchImpl); + if (repository?.default_branch !== source.ownedDefaultBranch) { + fail( + `${source.ownedRepository} default branch expected ${source.ownedDefaultBranch}, received ${repository?.default_branch}`, + ); + } + const refUrl = `${repositoryUrl}/git/ref/heads/${encodeURIComponent(source.ownedDefaultBranch)}`; + const ref = await fetchJson(refUrl, fetchImpl); + if (ref?.object?.sha !== source.testedHead) { + fail(`${source.ownedRepository} ${source.ownedDefaultBranch} expected ${source.testedHead}, received ${ref?.object?.sha}`); + } + } + return sources.length; +} + +async function main() { + const manifest = JSON.parse(readFileSync(defaultManifestPath, "utf8")); + const count = await verifyOwnedSources(manifest); + console.log(`Owned source evidence passed (${count} repositories).`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/lab/scripts/verify-owned-sources.test.mjs b/lab/scripts/verify-owned-sources.test.mjs new file mode 100644 index 0000000..16a8004 --- /dev/null +++ b/lab/scripts/verify-owned-sources.test.mjs @@ -0,0 +1,152 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { validateOwnedSourcesManifest, verifyOwnedSources } from "./verify-owned-sources.mjs"; + +const head = "a".repeat(40); +const reviewed = "b".repeat(40); +const base = "c".repeat(40); + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "scient-owned-sources-")); + const reviewRecord = "lab/external/upstream-reviews/review.md"; + mkdirSync(join(root, "lab/external/upstream-reviews"), { recursive: true }); + writeFileSync(join(root, reviewRecord), `Reviewed through: ${reviewed}\n`); + writeFileSync( + join(root, "lab/external/sources.lock.md"), + `| Scient source | inventory ${head} ${base} adapter-maintained |\n| Scient source | review ${head} ${reviewed} 2026-07-18 ${base} adapter-maintained |\n`, + ); + const source = { + ownedRepository: "ScientFactory/source", + sourceLockLabel: "Scient source", + ownedDefaultBranch: "main", + testedHead: head, + officialRepository: "Original/source", + officialDefaultBranch: "main", + reviewedThrough: reviewed, + reviewedAt: "2026-07-18", + integrationBase: base, + updateMode: "adapter-maintained", + reviewRecord, + }; + return { + root, + source, + manifest: { schema: "scient.owned-sources/v1", sources: [source] }, + maintainedSources: new Map([["scientfactory/source", "main"]]), + }; +} + +function remoteState(source) { + const { testedHead: _testedHead, ...state } = source; + return { + schemaVersion: 1, + ...state, + reviewRecord: `ScientFactory/Scient:${source.reviewRecord}`, + }; +} + +test("accepts matching source state and default-branch head", async () => { + const { root, source, manifest, maintainedSources } = fixture(); + const requests = []; + const fetchImpl = async (url, options) => { + requests.push({ url, authorization: new Headers(options.headers).get("Authorization") }); + return new Response( + JSON.stringify( + url.includes("raw.githubusercontent.com") + ? remoteState(source) + : url.endsWith(source.ownedRepository) + ? { default_branch: source.ownedDefaultBranch } + : { object: { sha: head } }, + ), + ); + }; + assert.equal( + await verifyOwnedSources(manifest, { root, fetchImpl, maintainedSources }), + 1, + ); + assert.equal(requests[0].authorization, null); + assert.equal(requests[1].authorization, null); + assert.equal(requests[2].authorization, null); +}); + +test("rejects a default branch that moved beyond the recorded tested head", async () => { + const { root, source, manifest, maintainedSources } = fixture(); + const fetchImpl = async (url) => + new Response( + JSON.stringify( + url.includes("raw.githubusercontent.com") + ? remoteState(source) + : url.endsWith(source.ownedRepository) + ? { default_branch: source.ownedDefaultBranch } + : { object: { sha: "d".repeat(40) } }, + ), + ); + await assert.rejects( + () => verifyOwnedSources(manifest, { root, fetchImpl, maintainedSources }), + /expected a{40}, received d{40}/, + ); +}); + +test("rejects a configured branch that is no longer the repository default", async () => { + const { root, source, manifest, maintainedSources } = fixture(); + const fetchImpl = async (url) => + new Response( + JSON.stringify( + url.includes("raw.githubusercontent.com") + ? remoteState(source) + : url.endsWith(source.ownedRepository) + ? { default_branch: "renamed-default" } + : { object: { sha: head } }, + ), + ); + await assert.rejects( + () => verifyOwnedSources(manifest, { root, fetchImpl, maintainedSources }), + /default branch expected main, received renamed-default/, + ); +}); + +test("rejects review evidence that does not contain the checkpoint", async () => { + const { root, manifest, maintainedSources } = fixture(); + writeFileSync(join(root, manifest.sources[0].reviewRecord), "No checkpoint here.\n"); + await assert.rejects( + () => verifyOwnedSources(manifest, { root, fetchImpl: fetch, maintainedSources }), + /does not contain reviewedThrough/, + ); +}); + +test("rejects human-readable source rows that drift from the manifest", async () => { + const { root, manifest, maintainedSources } = fixture(); + writeFileSync( + join(root, "lab/external/sources.lock.md"), + `| Scient source | inventory ${"e".repeat(40)} |\n| Scient source | review ${"e".repeat(40)} |\n`, + ); + await assert.rejects( + () => verifyOwnedSources(manifest, { root, fetchImpl: fetch, maintainedSources }), + /not aligned across sources.lock.md/, + ); +}); + +test("rejects stale non-head checkpoint fields in the source lock", () => { + const { root, manifest, maintainedSources } = fixture(); + writeFileSync( + join(root, "lab/external/sources.lock.md"), + `| Scient source | inventory ${head} ${base} adapter-maintained |\n| Scient source | review ${head} ${"d".repeat(40)} 2026-07-18 ${base} adapter-maintained |\n`, + ); + assert.throws( + () => validateOwnedSourcesManifest(manifest, root, maintainedSources), + /reviewedThrough is not aligned/, + ); +}); + +test("rejects omission of any explicitly maintained repository", () => { + const { root, manifest, maintainedSources } = fixture(); + maintainedSources.set("scientfactory/second-source", "dev"); + assert.throws( + () => validateOwnedSourcesManifest(manifest, root, maintainedSources), + /maintained repositories omitted.*scientfactory\/second-source/, + ); +});