Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions .changeset/named-lookup-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@bdsqqq/lnr-core": patch
"@bdsqqq/lnr-cli": patch
---

preserve initiative and roadmap lookup errors while retaining direct identifier
lookup before name fallback, including for initiative actions. fallback is allowed
only for a null result or an SDK InvalidInputLinearError whose nonempty parsed
errors all identify exactly the requested root. this is rejected-identifier
fallback, not proof that an entity is absent. network, structured permission-error
types, mixed and lazy-relation errors propagate; direct slug success does not
require list access. core direct lookups continue forwarding arbitrary strings.
7 changes: 7 additions & 0 deletions .changeset/nullable-read-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@bdsqqq/lnr-core": patch
---

preserve API and relationship errors in nullable read helpers instead of
misreporting failed reads as missing entities or preferences. genuine absent
results remain nullable.
7 changes: 7 additions & 0 deletions .changeset/project-creation-receipt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@bdsqqq/lnr-cli": patch
---

include the immutable id in successful project creation acknowledgements.
register sandbox ownership before independent list readback so a failed read
does not discard the returned id.
4 changes: 4 additions & 0 deletions .todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ test dispatch + payload wiring, not schema validation. requires DI refactor for
- [x] 0100 - pin cycle issue reads to the resolved immutable cycle id, with distinct selected/current fixtures and failure propagation
- verify filters, ordering, cursor traversal, archived data, and full json output; a first-page list is not full read parity
- [-] 0095 - verify operation errors and lifecycle behavior across the inventory --blocks 0098
- [x] 0124 - preserve initiative/roadmap detail errors with direct-first identifier precedence; name fallback only for genuine null or real SDK InvalidInputLinearError with nonempty uniform InvalidInput errors at the exact requested root; rejected identifier does not prove absence; preserve core arbitrary-string forwarding, optional owners and direct-slug mutation targets without list access; 866 core/real-core argv regressions pass, including intercepted SDK wrapping and mixed/lazy-error guards; negative control against rejected UUID dispatch: 516 fail/288 pass before adding 62 guard/absence controls; offline check/test (3227 pass)/api:check/build pass with cached nix Bun 1.3.13; nix develop --offline blocked by unavailable dependencies; no new live calls or pagination changes, full parity remains unaccepted
- [x] 0122 - propagate errors from 14 nullable read helpers; retain true missing roots/relations and production error classification, with 559 core/argv regressions; initiative/roadmap fallback contracts remain separate
- [x] 0120 - propagate only getIssue/getCycle detail errors; preserve missing roots, optional null relations, and unmatched cycle lookup; 116 core/real-core argv regressions cover error identity, production network/auth/permission/rate-limit/notfound classification, single fetches and name/number lookup; before-fix control: 90 fail, 26 pass; offline check/test (1723 pass)/api:check/build pass with cached nix Bun 1.3.13; no live verification or pagination changes
- [x] 0118 - propagate only document list errors; preserve Promise.all mapping and empty results; 36 core/real-core argv regressions cover original error identity, connection/project failures, JSON/quiet/table diagnostics and exit classification, payloads, and single fetches; before-fix control: 14 fail, 22 pass; offline check/test (1607 pass)/api:check/build pass with cached nix Bun 1.3.13; nix develop --offline blocked by unavailable dependencies; no live verification
- [x] 0117 - propagate only view/template list errors; preserve empty/missing-team and valid payloads; 111 core/real-core argv regressions use production error classification (network/auth/rate-limit/permission), including later-team failures and single attempts; before-fix control: 84 fail, 27 pass; offline check/test/api:check/build pass, no live verification; unrelated catches deferred
Expand All @@ -217,6 +219,8 @@ test dispatch + payload wiring, not schema validation. requires DI refactor for
- detect added/removed/changed operations, arguments, fields, enums, nullability, and deprecations; fail on unclassified changes
- retain explicit gaps in the report; prove checks catch missing dispatch, dropped payload fields, and unsupported operations
- [-] 0097 - make live verification fail honestly and isolate sandbox ownership --blocks 0098
- [x] 0123 - return project creation id and register fixture ownership before independent list/read assertions; offline verified, live proof/push held; does not recover #48's unknown-id project
- run 36641067664 (#48) left unknown-id project `e2e-project-ffecd613-ff51-410d-8751-12d00a58043e`; authorized read-only run 36645163754 found no accessible exact-name match, including archived. no deletion attempted; historical outcome remains unknown. later CI uses 0123 ownership receipts before readback
- [x] 0119 - add label-role/id receipts, independent original-assignment preconditions, batch CLI completion metadata, and immediate per-issue label observations without retrying writes or weakening assertions; offline extracted-body scenarios and safety tests/check pass, live cause remains unresolved
- [x] 0112 - separately authorized #40 view recovery: read-only run 36481626839 identified `54429643-f5fd-4ee9-b3bb-9948cf7245e8`; run 36483871681 deleted only that id once and verified archived-inclusive id-query absence; no retries. evidence: `docs/evidence/view-recovery-36483871681.json`
- run 36041211485 (#41) failed the original immediate cycle archive assertion, while all six subsequent SDK/raw observations confirmed archival of `7e61cc29-364d-48fa-9586-168d0f46f50b`; no unknown-id teardown warning in that run
Expand Down
17 changes: 17 additions & 0 deletions docs/evidence/missing-lookups-36650753534.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"run": "https://github.com/bdsqqq/lnr/actions/runs/36650753534",
"scope": "pinned bdsqqq-sandbox organization verified before fixed read-only probes",
"identifier": "00000000-0000-4000-8000-000000000099",
"roots": ["initiative", "roadmap"],
"observed": {
"status": 200,
"constructor": "InvalidInputLinearError",
"type": "InvalidInput",
"dataIsNull": true,
"parsedErrorType": "InvalidInput",
"parsedErrorPath": "exact root",
"rawExtensionType": "invalid input"
},
"limitation": "two missing-identifier observations, not evidence that every InvalidInput means absence or that permissions are granted",
"provenance": "selected allowlisted output manually transcribed from inspected CI logs; not an automated attestation"
}
6 changes: 3 additions & 3 deletions packages/cli/src/e2e-mutations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { executeApi, getClient, type Cycle, type GitAutomationState } from "@bdsqqq/lnr-core";
import { cleanupOwned, liveChildEnv, liveCredentials, registerViewReceipt, type OwnedFixture } from "./live-test-support";
import { cleanupOwned, liveChildEnv, liveCredentials, registerViewReceipt, registerProjectReceipt, type OwnedFixture } from "./live-test-support";

const { key: API_KEY, confirmOrg } = liveCredentials(process.env, true);
const client = getClient(API_KEY);
Expand Down Expand Up @@ -407,14 +407,14 @@ describe("e2e: mutations", () => {
const out = await lnr("project", "new", "--new-name", TEST_PROJECT_NAME, "--team", TEST_TEAM_KEY,
"--content", "e2e payload content", "--lead", "@me", "--priority", "0",
"--start-date", "2026-10-01", "--target-date", "2026-11-01", "--status", status.id);
projectId = registerProjectReceipt(out, projectFixture);
const json = await lnr("projects", "--json");
const projects = JSON.parse(json);
const matches = projects.filter((p: any) => p.name === TEST_PROJECT_NAME);
expect(matches).toHaveLength(1);
const testProject = matches[0];
expect(testProject).toBeTruthy();
projectId = testProject.id;
projectFixture.id = projectId;
expect(testProject.id).toBe(projectId);
expect(out).toContain("created");
const read = await client.client.rawRequest<{ project: unknown }, { id: string }>(
"query($id: String!) { project(id: $id) { id content startDate targetDate priority lead { id } status { id } } }",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/generated/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ async function handleCreateProject(input: ProjectInput): Promise<void> {
const project = await createProject(client, createPayload);

if (project) {
console.log(`created project: ${project.name}`);
console.log(`created project: ${project.name} (${project.id})`);
} else {
exitWithError("project creation returned no project; verify the outcome before retrying");
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/live-test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export function registerViewReceipt(output: string, fixture: OwnedFixture): stri
return fixture.id = id;
}

/** Preserve project ownership before listing or readback can fail. */
export function registerProjectReceipt(output: string, fixture: OwnedFixture): string {
const prefix = `created project: ${fixture.name} (`;
const id = output.startsWith(prefix)
? /^([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\)$/.exec(output.slice(prefix.length))?.[1]
: undefined;
if (!id || output !== `${prefix}${id})`) throw new Error("missing or invalid project creation receipt");
return fixture.id = id;
}

/** unknown write outcomes require manual recovery, never a speculative retry. */
export async function cleanupOwned(fixtures: OwnedFixture[]): Promise<void> {
const failures: Error[] = [];
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/src/project-receipt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, mock, test } from "bun:test";
import { cleanupOwned, registerProjectReceipt, type OwnedFixture } from "./live-test-support";

const id = "12345678-1234-1234-1234-123456789abc";
const name = "audit (project).*";
const receipt = `created project: ${name} (${id})`;

test("project receipt retains ownership when the independent listing fails", async () => {
const remove = mock(async (_id: string) => ({ success: true }));
const fixture: OwnedFixture = { name, remove };
const failure = new Error("listing failed");
const list = mock(async () => { throw failure; });
await expect((async () => {
expect(registerProjectReceipt(receipt, fixture)).toBe(id);
await list();
})()).rejects.toBe(failure);
expect(fixture.id).toBe(id);
await cleanupOwned([fixture]);
expect(remove).toHaveBeenCalledTimes(1);
expect(remove).toHaveBeenCalledWith(id);
});

for (const output of [
"", `created project: ${name}`, `created project: other (${id})`,
`created view: ${name} (${id})`, `created project: ${name} (not-a-uuid)`,
`prefix ${receipt}`, `${receipt} suffix`, `${receipt}\n`, `${receipt}\nextra`,
`${receipt}\n${receipt}`, receipt.replace(id, id.slice(1)),
]) {
test(`invalid project receipt leaves ownership unchanged: ${JSON.stringify(output)}`, () => {
const fixture: OwnedFixture = { name, remove: async () => ({ success: true }) };
expect(() => registerProjectReceipt(output, fixture)).toThrow("missing or invalid project creation receipt");
expect(fixture.id).toBeUndefined();
});
}
11 changes: 8 additions & 3 deletions packages/cli/src/router/initiatives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getClient,
listInitiatives,
getInitiative,
isRootIdentifierRejection,
findInitiativeByName,
getInitiativeUpdates,
getInitiativeExternalLinks,
Expand Down Expand Up @@ -35,7 +36,7 @@ export const listInitiativesInput = type({
});

export const initiativeInput = type({
nameOrId: type("string").configure({ positional: true }).describe("initiative name, slugId, or id"),
nameOrId: type("string").configure({ positional: true }).describe("initiative name, slugId, or id (direct identifier lookup takes precedence over name lookup)"),
"json?": type("boolean").describe("output as json"),
"quiet?": type("boolean").describe("output id only"),
"verbose?": type("boolean").describe("show all columns"),
Expand Down Expand Up @@ -129,8 +130,12 @@ export const initiativesRouter = router({
validateMutationOutput("initiative mutation", input);
}
const client = getClient();
let initiative = await getInitiative(client, input.nameOrId);

let initiative: Initiative | null = null;
try {
initiative = await getInitiative(client, input.nameOrId);
} catch (error) {
if (!isRootIdentifierRejection(error, "initiative")) throw error;
}
if (!initiative) {
initiative = await findInitiativeByName(client, input.nameOrId);
}
Expand Down
Loading
Loading