Skip to content
Merged
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
2 changes: 2 additions & 0 deletions openspec/changes/add-flow-index-only/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-25
65 changes: 65 additions & 0 deletions openspec/changes/add-flow-index-only/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Context

See `proposal.md`. Existing partial checkout already persists omitted-object
descriptors, but it also permits exact source materialization. Existing strict
checkout intentionally fails before all filesystem mutation. Neither surface
captures an unresolved boundary without changing source behavior.

## Goals / Non-Goals

**Goals:**

- Reuse the existing manifest, descriptor schemas, omission model, and shared
flow service.
- Add one explicit source-free operation with matching CLI and MCP adapters.
- Preserve current strict and partial checkout semantics byte-for-byte.

**Non-Goals:**

- Reading, reconstructing, or writing source bodies.
- Git, CI, merge-request, transport-release, or vendor-specific workflow
behavior.
- Treating an inexact object as materialized or exact.

## Decisions

### Use an explicit `index-only` operation rather than changing checkout defaults

The operation is opt-in because strict checkout's no-mutation guarantee is a
valuable safety boundary. Making partial checkout automatic would allow a
mixed transport to change source files while leaving unresolved components.
An index-only call makes the persistence intent visible and has no source-side
effect.

### Reuse the current manifest and omission descriptor formats

The service will build the same scoped manifest and apply existing selector
rules. It will create transport descriptors marked incomplete and omitted
descriptors only for entries that cannot be materialized. Exact entries remain
inventory-only until a normal checkout materializes them. This avoids a second
identity schema or source-resolution implementation.

### Expose thin CLI and MCP adapters over the public service

The CLI and MCP will parse the same explicit operation and delegate to the
same service result. They do not own persistence, SAP selection, or delivery
workflow, preserving the established parity boundary.

## Risks / Trade-offs

- An index can become stale after SAP history changes → normal checkout still
rebuilds authoritative provenance and replaces incomplete state only after
an exact result.
- Metadata selectors can require ADT metadata reads → bounded metadata reads
are allowed; source reads remain forbidden.
- Consumers may mistake inventory for source → result and descriptor state
identify omissions and the operation never reports source changes.

## Migration Plan

1. Release the additive service and adapter operation.
2. Consumers that need durable recovery state call index-only after a strict
boundary failure, then persist the resulting `.adt` state outside source
branches.
3. Roll back by stopping those calls; existing descriptors are safe to retain
or delete and normal checkout behavior is unchanged.
38 changes: 38 additions & 0 deletions openspec/changes/add-flow-index-only/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Why

Transport history can prove that an object cannot yet be materialized without
making the transport itself disappear from local recovery state. Callers need
to retain a safe, source-free inventory of that fact while keeping normal
checkout fail-closed and non-mutating.

## What Changes

- Add an explicit, vendor-neutral `adt-flow` index-only operation for a
transport scope.
- Persist deterministic `.adt` transport inventory and omission descriptors
without reading source bodies or changing format-owned source files.
- Expose the same operation through the flow CLI and MCP adapters, using the
shared service result and bounded diagnostics.
- Preserve existing checkout semantics: strict checkout still rejects an
inexact boundary before filesystem mutation, and partial checkout remains an
explicit source-materialization mode.

## Capabilities

### New Capabilities

- `adt-flow-index-only`: Persist source-free transport inventory and
non-materializable object diagnostics independently from source checkout.

### Modified Capabilities

- None.

## Impact

- Affected packages: `@abapify/adt-flow`, `@abapify/adt-cli`, and
`@abapify/adt-mcp`.
- Additive public API and command/tool surface; no dependency, release, or
transport-system specific behavior.
- Rollback consists of removing the additive command/tool and its descriptors;
existing checkout and source files are unaffected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Purpose

Provide a durable, source-free record of transport inventory and unresolved
source boundaries without weakening exact source checkout guarantees.

## ADDED Requirements

### Requirement: Flow can index a transport without materializing source

`adt-flow` SHALL provide an explicit index-only operation for a transport scope
that persists deterministic `.adt` transport and object descriptors without
reading source bodies or changing format-owned source paths.

#### Scenario: Inexact source boundary is indexed

- **GIVEN** a transport contains a relevant object whose source boundary is
inexact
- **WHEN** an index-only operation is requested for that transport
- **THEN** the transport inventory and an omitted-object descriptor retain the
object identity, component, source transport, and bounded diagnostic
- **THEN** no source body is read and no format-owned source path is changed

#### Scenario: Exact source remains unmaterialized during indexing

- **GIVEN** a transport contains an exact source component
- **WHEN** an index-only operation is requested
- **THEN** the transport inventory is persisted without selecting or writing
the component's source files

#### Scenario: Normal checkout remains strict

- **GIVEN** a transport contains a relevant object whose source boundary is
inexact
- **WHEN** normal checkout is requested without the index-only operation
- **THEN** checkout fails with its typed bounded diagnostic before any
repository path is changed

### Requirement: Index-only flow is available through equivalent adapters

The CLI and MCP flow adapters SHALL expose the same explicit index-only
operation and SHALL return equivalent structured results without source bodies
or credentials.

#### Scenario: CLI and MCP index the same fixture

- **GIVEN** the CLI and MCP receive the same flow configuration, transport
manifest, and repository tree
- **WHEN** each requests index-only flow for the transport
- **THEN** both return equivalent inventory, descriptor, and omission results
- **THEN** neither changes format-owned source paths
16 changes: 16 additions & 0 deletions openspec/changes/add-flow-index-only/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## 1. Flow service and descriptors

- [x] 1.1 Add a failing `adt-flow` service test for an inexact manifest indexed without source reads or source-file changes.
- [x] 1.2 Implement the explicit index-only service operation by reusing manifest, inventory, omission, and descriptor logic; verify the focused `adt-flow` test passes.
- [x] 1.3 Add a regression test that strict checkout still leaves the repository unchanged on the same inexact manifest; verify it passes.

## 2. Delivery adapters

- [x] 2.1 Add failing CLI command tests for index-only transport flow and its source-free structured output.
- [x] 2.2 Implement the CLI adapter as a thin delegation to the public flow service; verify CLI tests pass.
- [x] 2.3 Add MCP parity coverage for the same fixture and implement the matching tool delegation; verify the parity test passes.

## 3. Documentation and verification

- [x] 3.1 Document the index-only command and the distinction from strict and partial checkout; verify the README example is accurate.
- [x] 3.2 Run focused Nx test, typecheck, lint, build, format-check, OpenSpec strict validation, and `git diff --check`; record any unrelated baseline blocker.
28 changes: 28 additions & 0 deletions packages/adt-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ adt flow checkout tr DEVK900001
adt flow checkout tr DEVK900001,DEVK900002 --base
```

To persist only transport inventory and unresolved-boundary descriptors under
`.adt`, without reading or materializing source files, use the explicit
index-only command:

```text
adt flow index tr DEVK900001,DEVK900002
```

`checkout` remains strict: if any versioned component has no exact source
boundary, it fails without changing the workspace. `checkout --partial` is a
separate explicit opt-in that materializes only exact objects. `index` never
materializes source; it retains the complete transport inventory and records
every unresolved component as an `omitted` descriptor for a later retry.

Automation that must preserve an inexact transport for a later retry can opt
into `adt flow checkout tr <transport> --index-on-inexact`. Only the typed
`manifest_inexact` outcome falls back to source-free indexing; every other
checkout error remains fail-closed.

```typescript
import {
createAdtFlowService,
Expand All @@ -53,6 +72,15 @@ await flow.checkout({
include: { objectTypes: ['CLAS', 'INTF'] },
},
});

await flow.index({
root: process.cwd(),
transports: ['DEVK900001'],
config: {
format: { id: 'abapgit', options: { folderLogic: 'prefix' } },
include: { objectTypes: ['CLAS', 'INTF'] },
},
});
```

The service invokes no Git command. It reconciles format-owned files and
Expand Down
64 changes: 63 additions & 1 deletion packages/adt-flow/src/commands/flow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { randomUUID } from 'node:crypto';

Check notice on line 1 in packages/adt-flow/src/commands/flow.ts

View check run for this annotation

CodeScene Access / CodeScene Code Health Review (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 6.50 to 6.08, threshold = 4 This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.

Check notice on line 1 in packages/adt-flow/src/commands/flow.ts

View check run for this annotation

CodeScene Access / CodeScene Code Health Review (main)

✅ Getting better: Primitive Obsession

The ratio of primitive types in function arguments decreases from 64.71% to 57.89%, threshold = 30.0% The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.

Check notice on line 1 in packages/adt-flow/src/commands/flow.ts

View check run for this annotation

CodeScene Access / CodeScene Code Health Review (main)

✅ Getting better: String Heavy Function Arguments

The ratio of strings in function arguments decreases from 64.71% to 57.89%, threshold = 39.0% The functions in this file have a high ratio of strings as arguments. Avoid adding more.
import { lstatSync, readlinkSync, realpathSync } from 'node:fs';
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
Expand Down Expand Up @@ -263,6 +263,11 @@
description:
'Write skipped-object JSON after a successful partial checkout',
},
{
flags: '--index-on-inexact',
description:
'Persist source-free inventory when an exact source boundary is unavailable',
},
],
async execute(args, ctx) {
if (!ctx.getAdtClient) {
Expand Down Expand Up @@ -291,7 +296,7 @@
// Commander normalizes --partial-report to partialReport at runtime;
// retain the dashed spelling for direct plugin callers and tests.
const report = partialReportPath(
args.partialReport ?? args['partial-report'],
args['partialReport'] ?? args['partial-report'],
ctx.cwd,
realRoot,
);
Expand All @@ -306,6 +311,7 @@
transports: transports(args['transport']),
mode: args['base'] === true ? 'base' : 'head',
partial: args['partial'] === true,
...(args['indexOnInexact'] === true ? { indexOnInexact: true } : {}),
config,
});
if (report) await writePartialReport(report, realRoot, result);
Expand All @@ -327,6 +333,56 @@
};
}

function indexTrCommand(
dependencies: FlowCommandDependencies,
): CliCommandPlugin {
return {
name: 'tr',
description: 'Index transport inventory without materializing source',
arguments: [
{
name: '<transport>',
description: 'Transport number or comma-separated transport scope',
},
],
async execute(args, ctx) {
if (!ctx.getAdtClient) {
throw new AdtFlowError(
'sap_operation_failed',
'An authenticated ADT client is required.',
);
}
const config = flowConfig(ctx);
const format = dependencies.getFormat(config.format.id);
if (!format) {
throw new AdtFlowError(
'format_unsupported',
`Format plugin "${config.format.id}" is not registered.`,
);
}
const client = (await ctx.getAdtClient()) as AdtClient;
const result = await dependencies.createService(client, format).index({
root: ctx.cwd,
transports: transports(args['transport']),
config,
});
ctx.logger.info(
`Indexed ${result.requestedTransports.join(', ')}: ` +
`${result.descriptors.length} descriptors, ${result.skipped.length} omissions.`,
);
for (const skipped of result.skipped) {
ctx.logger.warn(
`Indexed omission ${skipped.object} (${skipped.component}; ${skipped.diagnostic}).`,
);
}
ctx.logger.info(
`SAP calls: manifest=${result.sapCalls.manifest}, metadata=${result.sapCalls.metadata}, ` +
`source=${result.sapCalls.source}.`,
);
},
};
}

export function createFlowCommand(
overrides: Partial<FlowCommandDependencies> = {},
): CliCommandPlugin {
Expand All @@ -340,6 +396,12 @@
description: 'Reconcile a source tree to an ADT boundary',
subcommands: [checkoutTrCommand(dependencies)],
},
{
name: 'index',
description:
'Persist transport inventory without source materialization',
subcommands: [indexTrCommand(dependencies)],
},
],
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/adt-flow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {
AdtFlowError,
type FlowCheckoutDependencies,
type FlowCheckoutInput,
type FlowIndexInput,
type FlowCheckoutMode,
type FlowCheckoutResult,
type FlowErrorCode,
Expand Down
Loading
Loading