Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions launchpad/src/diagnostics-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3271,3 +3271,44 @@ test("Bun PATH readiness accepts a separate exact-version installation and rejec
expect(bunPathCheck({ pathExecutable: "/custom/manager/bun", requiredVersion: "1.4.1", run: () => ({ ok: true, stdout }) }).status).toBe(status);
}
});


async function appReadinessFixture({ managed = false, contract = null } = {}) {
const root = await createCompaniesWorkspaceFixture();
const organizationRoot = join(root, "organizations/OmegaCo_GEN3");
const target = join(organizationRoot, "design-system");
await mkdir(target, { recursive: true });
await mkdir(join(organizationRoot, "company/colleagues"), { recursive: true });
await mkdir(join(organizationRoot, "manual"), { recursive: true });
await writeJson(join(organizationRoot, "company.gen3.json"), {
organization_generation: "gen3", company: { slug: "OmegaCo", display_name: "OmegaCo", github_org: "OmegaCo" },
workspaces: [{ slug: "workspace", default: true }], layers: [{ path: "design-system", kind: "root-docs", ownership: "manual" }],
});
await writeJson(join(organizationRoot, "modules.manifest.json"), {
organization_generation: "gen3", company: "OmegaCo", github_org: "OmegaCo", module_slots: [{
path: "design-system", slug: "design-system", space: "root", status: "active", default_access: "expected",
materialization: managed ? "doctor_managed_nested_repo" : undefined,
git: { url: "git@github.com:OmegaCo/design-system.git", branch: "main" },
}],
});
if (contract) await writeJson(join(target, "lazurio.module.json"), contract);
return { root, target, read: () => buildLaunchpadAppsResponse({ companiesRoot: root, launchpadRoot: join(root, "launchpad"), runtimeManager: { appsWithRuntime: async (apps) => apps } }) };
}
test("managed app parent with only db is not available merely because its directory exists", async () => {
const fixture = await appReadinessFixture({ managed: true });
await mkdir(join(fixture.target, "db"));
const response = await fixture.read();
expect(response.organizations[0].organization_modules[0]).toMatchObject({ status: "quarantined", readiness: { severity: "blocking", reason: "managed_checkout_not_repository" } });
expect(response.organizations[0].space_readiness.blocking_slots).toContainEqual(expect.objectContaining({ path: "design-system", reason: "managed_checkout_not_repository" }));
});
test("missing declared App blocks readiness but explicit data-only does not", async () => {
const fixture = await appReadinessFixture({ contract: {
schema_version: "lazurio.module.v1", id: "design-system", company: "OmegaCo", tcp_port_policy: { mode: "none" }, port_leases: [], apps: ["app/package.json"], default_app: "app/package.json",
} });
const missing = await fixture.read();
expect(missing.organizations[0].space_readiness.blocking_slots).toContainEqual(expect.objectContaining({ reason: "declared_app_unavailable" }));
await writeJson(join(fixture.target, "lazurio.module.json"), { schema_version: "lazurio.module.v1", id: "design-system", company: "OmegaCo", tcp_port_policy: { mode: "none" }, port_leases: [], apps: [] });
const dataOnly = await fixture.read();
expect(dataOnly.organizations[0].organization_modules[0].apps.state).toBe("explicit-none");
expect(dataOnly.organizations[0].space_readiness.blocking_slots).toEqual([]);
});
18 changes: 18 additions & 0 deletions launchpad/src/lazurio-update-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3006,3 +3006,21 @@ function runGit(cwd, args) {
function runGitResult(cwd, args) {
return spawnSync("git", args, { cwd, encoding: "utf8", shell: false });
}


test("declared managed directory-only checkout blocks both Sync and GET readiness", async () => {
const root = await mkdtemp(join(tmpdir(), "lazurio-db-first-status-")); cleanup.push(root);
const organization = repo("TestCo::root", "organization_root", "TestCo", "root");
organization.absolute_path = join(root, "organizations", "TestCo");
const parent = repo("TestCo::mission-control", "root_repo", "TestCo", "mission-control");
parent.absolute_path = join(organization.absolute_path, "mission-control");
parent.slot_path = "mission-control";
parent.materialization = "doctor_managed_nested_repo";
await mkdir(join(parent.absolute_path, "db"), { recursive: true });
const inspectLocalRepo = async () => ({ ok: true, directoryOnly: true, dirtyPaths: [] });
expect(await updateManagedRepo(parent, { deps: { inspectLocalRepo } })).toMatchObject({ state: "blocked", reason: "managed_checkout_not_repository" });
expect(await readLazurioUpdateStatus({ rootPath: root, deps: {
inspectLocalRepo, buildInventory: async () => ({ repos: [organization, parent], warnings: [] }),
} })).toMatchObject({ state: "blocked", checked_remote: false, reason: "managed_checkout_not_repository", repo_key: parent.key });
expect(await updateManagedRepo({ ...parent, materialization: null }, { deps: { inspectLocalRepo } })).toMatchObject({ state: "current", reason: "directory_only" });
});
33 changes: 33 additions & 0 deletions lazurio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,36 @@ Existující vyhovující instalace je platná bez ohledu na instalační správ
Agent JSON consumer musí číst schema version; v1 schema zůstává historicky
zachované, ale nový installer vydává v2 s osmi kroky. Zelený version probe
neprokazuje přihlášení k modelu ani celý onboarding Organizace.


### Obnova db-first Mission Control instalace

Pouhá složka deklarovaného `doctor_managed_nested_repo` není hotový checkout.
Update i lokální status ji hlásí jako `managed_checkout_not_repository`;
Doctor ověřuje vlastní Git root a očekávané App projekce. Chybějící nebo
neplatná deklarovaná App blokuje readiness. Výslovné `apps: []` dál označuje
legitimní datový modul a nevyžaduje runtime.

Explicitní `lazurio organization install <github-login>` na macOS umí doplnit
chybějící Mission Control app-code, pokud parent obsahuje právě existující
canonical `db/`. Nejprve projde standardní immutable Organization identitou,
manifesty a ověřením db jako vlastního čistého Git checkoutu s přesným originem
na deklarované `v3`. Neznámé soubory, symlink, cizí nebo dirty db blokují zápis.
Staged clone musí patřit deklarovanému Organization app repu, ignorovat `db/`
a nesmí pod ním trackovat obsah.

Core přes systémové macOS JXA volá `renameatx_np(RENAME_EXCL)` nad otevřenými
directory descriptors: app položky se přesouvají bez přepsání existujícího
cíle, `.git` se aktivuje poslední. Canonical db zůstává na stejné cestě/inode
se stejným HEADem a není kopírované, přesunuté, fetchované ani publikované.
Při selhání se vlastní app položky vracejí do stagingu; souběžně změněný obsah
se nemaže. Změněné `.git` zůstává v karanténě, aby neaktivovalo neúplnou app.
Staging se zachovává i při selhání ověření před publikací. Recovery checkout a přesný blocker zůstávají v reportu k ověření
Agentem. Pád procesu může zanechat neaktivované app položky: další běh je
nepřebírá odhadem, ale vrátí blocker. Po úspěchu může zůstat prázdný staging
adresář; nevzniká nový runtime stav ani druhý databázový locator.

Tato úzká obnova existujícího db-first parentu je zatím **macOS-only**.
Jiné platformy vrátí `repository_parent_recovery_platform_unsupported` před
mutací tohoto parentu. Standardní instalace do chybějícího cíle i validní již
existující checkouty dál používají původní přenositelný postup.
46 changes: 38 additions & 8 deletions lazurio/core/git-materialization-lib.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { inspectRepositoryContainer, publishRepositoryContainer } from "./repository-container-publisher-lib.mjs";
import { lstat, mkdir, mkdtemp, readdir, realpath, rename, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
Expand All @@ -11,6 +12,7 @@ import {
export const GIT_CHECKOUT_MATERIALIZATION_MODES = Object.freeze([
"organization-root",
"nested-repo",
"repository-db-parent",
]);
export const GIT_CLONE_TIMEOUT_MS = 10 * 60_000;
export const GIT_FETCH_TIMEOUT_MS = 20_000;
Expand Down Expand Up @@ -82,7 +84,11 @@ export async function materializeGitCheckout({
beforeStage = async () => {},
beforePublish = async () => {},
} = deps;
if (await pathEntry(absoluteTargetPath)) return targetExists();
let existingContainer = null;
if (mode === "repository-db-parent") {
try { existingContainer = inspectRepositoryContainer(absoluteTargetPath, "db"); }
catch (error) { return verificationFailure("Existující db-first parent nelze bezpečně doplnit.", error.code); }
} else if (await pathEntry(absoluteTargetPath)) return targetExists();

const targetParent = dirname(absoluteTargetPath);
const targetName = basename(absoluteTargetPath);
Expand All @@ -91,6 +97,12 @@ export async function materializeGitCheckout({
let stagingName = null;
let expectedParentRealPath = null;
let materialized = false;
// Every existing-container failure retains the stage, including failures
// during verification: concurrent content may already have appeared there.
const preserveStaging = Boolean(existingContainer);
const recoveryFailure = (result) => existingContainer && stagingPath
? { ...result, message: `${result.message} Recovery checkout zůstal v ${stagingPath}.` }
: result;
try {
transportCwd = await makeTempDirectory(join(tmpdir(), "lazurio-git-transport-"));
const source = await run(
Expand Down Expand Up @@ -165,7 +177,7 @@ export async function materializeGitCheckout({
targetPath: stagingPath,
});
if (!stagingBoundary.ok) {
return boundaryFailure("Dočasný checkout nevznikl ve fyzicky ověřeném rodiči targetu.");
return recoveryFailure(boundaryFailure("Dočasný checkout nevznikl ve fyzicky ověřeném rodiči targetu."));
}
// Keep the bandwidth-saving single-branch bootstrap, but leave every
// published checkout with the canonical all-branches fetch contract that
Expand All @@ -184,15 +196,15 @@ export async function materializeGitCheckout({
},
);
if (!fetchConfiguration.ok) {
return verificationFailure("Naklonovaný checkout nemá kanonický fetch kontrakt.");
return recoveryFailure(verificationFailure("Naklonovaný checkout nemá kanonický fetch kontrakt."));
}
const gitVerification = await verifyClonedCheckout({
path: stagingPath,
branch,
remote,
run,
});
if (!gitVerification.ok) return gitVerification;
if (!gitVerification.ok) return recoveryFailure(gitVerification);

const policyVerification = await verifyStaged({
mode,
Expand All @@ -202,10 +214,10 @@ export async function materializeGitCheckout({
head: gitVerification.head,
});
if (!policyVerification?.ok) {
return verificationFailure(
return recoveryFailure(verificationFailure(
policyVerification?.message ?? "Naklonovaný checkout neprošel ověřením identity vlastníka.",
policyVerification?.code,
);
));
}

await beforePublish({
Expand All @@ -219,8 +231,14 @@ export async function materializeGitCheckout({
expectedParentRealPath,
stagingName,
targetName,
existingContainer,
});
if (!publication?.ok) {
if (existingContainer) {
// The recovery publisher may have quarantined a concurrent edit. Never
// recursively discard that evidence after a failed multi-item publish.
return recoveryFailure(verificationFailure("Doplnění app-code selhalo; db zůstalo na místě.", publication?.code ?? "repository_parent_recovery_failed"));
}
if (publication?.code === "target_exists") return targetExists();
if (publication?.code === "target_case_collision") return targetCollision();
if (["parent_identity_changed", "staging_identity_changed"].includes(publication?.code)) {
Expand All @@ -241,12 +259,12 @@ export async function materializeGitCheckout({
remote,
};
} catch (error) {
return cloneFailure({ error: error instanceof Error ? error.message : String(error) });
return recoveryFailure(cloneFailure({ error: error instanceof Error ? error.message : String(error) }));
} finally {
if (transportCwd) {
await remove(transportCwd, { recursive: true, force: true }).catch(() => {});
}
if (stagingName && expectedParentRealPath && !materialized) {
if (stagingName && expectedParentRealPath && !materialized && !preserveStaging) {
try {
await discard({
targetParent,
Expand Down Expand Up @@ -279,6 +297,7 @@ async function publishCheckoutWithPinnedParent({
expectedParentRealPath,
stagingName,
targetName,
existingContainer = null,
}) {
return runPinnedCheckoutOperation({
mode: PINNED_CHECKOUT_PUBLISH_MODE,
Expand All @@ -287,6 +306,7 @@ async function publishCheckoutWithPinnedParent({
expected_parent_real_path: expectedParentRealPath,
staging_name: stagingName,
target_name: targetName,
existing_container: existingContainer,
},
failureCode: "publisher_failed",
});
Expand Down Expand Up @@ -354,6 +374,16 @@ export async function runPinnedCheckoutPublisher() {
if (!isSamePath(dirname(stagingRealPath), actualParentRealPath)) {
throw pinnedPublisherError("staging_identity_changed");
}
if (payload.existing_container) {
const result = await publishRepositoryContainer({
stagingPath: join(actualParentRealPath, payload.staging_name),
targetPath: join(actualParentRealPath, payload.target_name),
expectedContainer: payload.existing_container,
});
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.ok) process.exitCode = 1;
return;
}
if (await lstatOrNull(payload.target_name)) {
throw pinnedPublisherError("target_exists");
}
Expand Down
27 changes: 27 additions & 0 deletions lazurio/core/git-materialization-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,38 @@ import {
} from "./git-materialization-lib.mjs";

const roots = [];
const macTest = process.platform === "darwin" ? test : test.skip;

afterAll(async () => {
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
});

macTest("db-first verification failure preserves its stage and concurrent contents", async () => {
const fixture = await fixtureRemote();
const target = join(fixture.root, "organizations", "DbFirst_GEN3");
await mkdir(join(target, "db"), { recursive: true });
const dbBefore = await lstat(join(target, "db"));
let stagedPath;
let discarded = false;
const result = await materializeGitCheckout({
mode: "repository-db-parent", boundaryRoot: fixture.root, targetPath: target,
remote: fixture.remote, branch: "main", run: runGit,
runPinnedChild: runGitInPinnedTemporaryChild, remoteEnvironment: safeGitRemoteEnv(),
deps: { discard: async () => { discarded = true; } },
verifyStaged: async ({ path }) => {
stagedPath = path;
await writeFile(join(path, "concurrent.txt"), "preserve");
return { ok: false, code: "injected_verification_failure" };
},
});
expect(result).toMatchObject({ ok: false, code: "injected_verification_failure" });
expect(result.message).toContain(stagedPath);
expect(discarded).toBe(false);
expect(existsSync(join(stagedPath, "concurrent.txt"))).toBe(true);
expect(await readdir(target)).toEqual(["db"]);
expect((await lstat(join(target, "db"))).ino).toBe(dbBefore.ino);
});

test("one Core primitive publishes an Organization root only after owner verification", async () => {
const fixture = await fixtureRemote();
const target = join(fixture.root, "organizations", "ExampleOrganization_GEN3");
Expand Down
Loading