Skip to content

Give converted programs a coherent home, and run five of them as a service - #154

Open
Gustav Kaleta (gkaleta) wants to merge 22 commits into
mainfrom
feat/service-namespaces
Open

Gustav Kaleta (gkaleta) wants to merge 22 commits into
mainfrom
feat/service-namespaces

Conversation

@gkaleta

@gkaleta Gustav Kaleta (gkaleta) commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Converted programs had nowhere coherent to live, nothing demonstrated what a converted service looks like running, and several things found along the way were broken.


1 — Assign each program a namespace instead of asking for one

The prompts asked for package com.example.something / namespace CobolMigration.Something. Across 162 generated files that produced 33 packages under 5 unrelated roots, with shared copybook records re-emitted under each (Qpipccab x4, Kygfc002 x4).

A second cause was policy: the shared-copybook rule told the model to nest a shared layout inside every caller, so one copybook became four records free to drift apart.

Now: the namespace is derived. Service = the source folder; a generic container (SRC) yields to its parent; shared copybook types live in one shared namespace.

Measured on the bd conversion: 33 packages under 5 roots → 6 under 1, 27 shared types in one place, 117 files, zero outside the assigned root.

2 — Let an estate choose the shape

The root was configurable; the structure beneath it was not — ForProgram could only ever return root.service. DDD had no way in.

TARGET_ARCHITECTURE now takes service (default, unchanged), ddd/layered, flat, or custom with TARGET_NAMESPACE_TEMPLATE. ai-config.env.example carries worked examples showing the namespace each produces, in both languages.

3 — Name the contract for a called program

Seven files declared IBdsmfjlService for the same module, and three of them declared three different methods on it. A COBOL program has one entry point, so the interface has one method and both are derivable. One program is made responsible for declaring each — the callee when it is in the drop, otherwise the first caller in a stable order.

4 — A running service built from the bd modules

Five COBOL function modules as HTTP endpoints, each naming its origin program, with the UI built from /api/catalog. No customer code is committedsource/ and output/ stay gitignored; this holds the service shape, not the record layouts.

Two deliberate differences, marked in code: the reconciliation batch required both inputs pre-sorted and 1:1 (a property of the job, not the data) — matching on key means unsorted input reconciles correctly instead of silently producing wrong output; and a zero-length batch reports zero throughput rather than dividing by zero.


Found and fixed along the way

demos/ broke the toolchain build — the root project globs **/*.cs and excluded the portal and tests but not this fixed
Converting 5 programs analysed all 101 estate copybooks, including 36 from an unrelated application — now follows the COPY graph (65) fixed
The repo's COPY regex follows commented-out COPY lines — a test caught it; the column-7 indicator is now tested by position fixed
./doctor.sh reverse-eng --program X never worked — doctor.sh always passed --programs to a command that never declared it fixed
Every conversion overwrote the last — runs now write to output/csharp/20260917-140000, readers resolve the newest fixed
Five portal endpoints ignored REPO_ROOT and looked for the estate inside the portal's own directory — a reverse-engineering report generated minutes earlier was reported as not existing fixed

Two checks, because prompts can be edited away

The namespace is assigned in an injected block, but the line telling the model to use it sits in the User section Prompt Studio rewrites. If an AI rewrite drops it, nothing fails — the model quietly reverts to inventing a root.

  • NamespaceCompliance verifies the output: every generated file must declare a namespace under the configured root, tested on segment boundaries. Verified against all 117 files — zero deviations.
  • GeneratedTypeCollisions reports types two files declare in the same namespace. It reports; it does not rewrite — choosing which of three contracts is correct is not a decision for a post-pass.

Testing

390 toolchain + 113 portal + 55 demo portal, all passing. Verified live against the real 41-program estate and a full C# conversion of the 5 bd programs.

Known limitation — measured, not estimated

The generated C# still has same-namespace type collisions and will not compile as-is. A full C# conversion of the 5 bd programs was run before and after the call-target work:

Files Colliding type names Per file
Before call-target contracts 117 48 0.41
After 70 22 0.31

An improvement, but not a fix, and the detail matters more than the total:

Bdsdatoi          x7   in Modernized.Banking.Shared    ← worse than before
IBdsmfjlService   x6   in Modernized.Banking.Bd        ← unchanged
BdsdfdtService    x4

IBdsmfjlService is untouched because CallTargetRegistry scans program files only, and these declarations come from copybook conversions, which never receive the contract block.

Bdsdatoi is worse: a shared copybook type in .Shared, declared seven times. The shared-types rule instructs the model to reference rather than declare, and it declares anyway.

What this establishes: prompt-level enforcement does not survive 70 independent conversions. Two attempts — shared copybooks, then call targets — both leak. The deterministic fix is to generate shared types once from REKT data_structures and have every program reference them, which also removes invention from the shared layer entirely. That is the follow-up, not this PR.

This is not a regression against main: per-program packages hid the same duplication as four divergent copies of one record. GeneratedTypeCollisions names them until the generator exists.

The converter prompts asked for "package com.example.something" and "namespace
CobolMigration.Something", leaving the model to invent a root per program. Across 162
generated files that produced 33 packages under five unrelated roots — com.example,
com.bank, com.legacy, com.bbva and com.modernized. A service cannot be assembled from
that, and an earlier fix only moved each file to wherever its own declaration pointed.

The namespace is now derived rather than requested. A program's service is the folder it
occupies in the source drop, which is how these estates are already organised, so the
same program lands in the same place on every run. A generic container directory yields
to its parent, because naming a service "src" says nothing about it. The root is
configurable through TARGET_ROOT_NAMESPACE for estates with a house convention.

The shared-copybook rule is also inverted. It previously told the model to nest a shared
layout inside each program that used it, explicitly to dodge name collisions; the result
was four independent copies of the same record, free to drift apart. Types built from a
copybook used by more than one program now live in one shared namespace that every
service references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Helpers/RektPromptInjector.cs Fixed
The estate under source/bd is five COBOL function modules: the banking-date lookup, its
error-text companion, a batch throughput display, the shared database error handler, and
an interest-rate reconciliation batch. Reviewing a conversion of them means reading
generated code and asking whether the behaviour survived. This makes the question
answerable by calling it instead.

Each capability names the program it came from, in the catalogue and in the interface,
because "is this still the same behaviour" cannot be checked against an anonymous
endpoint. The browser UI is built from that catalogue, so a capability that is not served
cannot appear in it.

Two differences from the original are deliberate and marked in the code. The
reconciliation batch required both inputs sorted and one-to-one, which is a property of
the job that produced the files rather than of the data; matching on the key means an
unsorted input reconciles correctly instead of silently producing wrong output. A
zero-length batch run reports zero throughput rather than dividing by zero.

No customer code is committed. source/ and output/ remain ignored; this project holds the
service shape those modules describe, not the customer's record layouts or business
constants, and it does not stand in for the converted output the pipeline produces.

Tests: 35 domain cases covering calendar arithmetic across holiday runs, retry
classification, and reconciliation of unsorted input; 20 HTTP cases asserting that every
advertised endpoint answers, that a rejection carries its reason in the body, and that the
sample the interface ships with produces every outcome. The HTTP tests caught enums
serialising as numbers, which would have shown the user "1" instead of "Weekend".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
@gkaleta Gustav Kaleta (gkaleta) changed the title Assign each converted program a namespace instead of asking for one Give converted programs a coherent home, and run five of them as a service Sep 16, 2026
Comment thread demos/BankingServicePortal.Tests/ApiEndpointTests.cs Fixed
The toolchain project compiles **/*.cs and explicitly excludes the portal and the two
test projects, each of which has its own project file. demos/ is the same kind of thing
and was not excluded, so adding it made the toolchain compile the demo's sources and
its generated assembly attributes, which collide with the toolchain's own.

The failure only appears once demos/ has been built at least once, which is why it
survived the first build of the branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Selecting a program kept every copybook in the source drop. The reasoning was sound —
a program without its record layouts is converted against layouts the model invents,
and a COPY may be nested inside another copybook — but the remedy was blunt. On a
source drop holding two unrelated applications, converting five programs of one meant
analysing all 101 copybooks, including 36 belonging to the other.

The COPY graph is followed instead, from the chosen programs through whatever their
copybooks themselves copy. That keeps the case the blanket rule existed for while
leaving behind copybooks nothing in the selection reaches: 65 rather than 101 for the
five-program service this was measured on.

Two properties worth naming. A copybook name shared by two files keeps both, because
dropping one picks a record layout on the caller's behalf. An unresolved COPY target
is simply absent — that is a missing copybook, which the scan already reports, and it
must not stop the rest of the closure being collected.

The COPY pattern used elsewhere in this repository folds the comment check into the
regex, where it can backtrack around the column-7 indicator and follow a COPY that was
deliberately commented out. A test caught that; the indicator is tested by position.

Tests: seven new cases — the copybooks a program reaches, a copybook nothing reaches
being left behind, a copybook reached only through another, a cycle, an unresolved
target alongside a resolved one, a shared name keeping both, and a commented-out COPY
not being followed. Toolchain suite 318 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Helpers/ProgramSelection.cs Fixed
Giving a service's programs one namespace is what makes them a service, but it removes
the accident that was hiding a real problem. With a package per program, two programs
could each invent a type of the same name and nothing objected — that is how one estate
ended up with four independent versions of the same copybook record. Sharing a namespace
turns the same duplication into a build error.

Measured on the five-program service converted with the new policy: 30 type names are
declared by more than one file. The duplicates are rarely identical. Seven files each
declare IBdsmfjlService for the same callee, and the three examined declare three
different methods on it — ExecuteAsync, ReportAsync and ErrorCodemeldAsync. That is not
redundancy to delete, it is three callers describing one callee three ways, and it was
equally true before this policy; only now does anything say so.

The detector names the namespace, the type and the files that declare it. Depth is
tracked by braces rather than indentation because a file-scoped namespace puts its
top-level types exactly where a block-scoped one puts nested ones — the first version
of this used indentation and reported every nested type as a collision.

This reports; it does not rewrite. Choosing which of three contracts is correct is not
a decision to take from a generated-code post-pass.

Tests: 17 cases — clean estates, a two-file collision, the same name in different
namespaces, all five declaration kinds, nested types under both namespace styles,
modifiers, recursion, ordering by severity, a missing directory, and the report text.
Toolchain suite 335 passed. Verified against the real generated output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Nine findings, three of which were worth acting on and six of which were noise from a
rule that cannot see intent.

The COPY-target scan read as a loop with two guard clauses; as a query the comment test
is named and the shape says what it does. The catalogue test extracted its endpoints
inside the loop that requested them; the extraction is now a projection, leaving the
loop doing only the awaiting it cannot avoid.

The empty catch in a test's Dispose swallowed the reason a temp directory could not be
removed. A directory the operating system still holds open is not a test failure, but
silence means a leak is never noticed either, so the reason is written to stderr.

Path.Combine in the tests is replaced by Path.Join: every segment there is a relative
name the test itself builds, so Combine's argument-dropping could only ever hide a
mistake. The call in RektPromptInjector keeps Combine and says why — COBOL_SOURCE_FOLDER
may be absolute, and an absolute source folder is meant to win outright rather than be
appended to the repository root. That is the same reason the existing calls in
RektContextLoader and StubCopybookCatalog keep it.

Toolchain suite 335 passed, demo portal 55 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
…to invent one

The prompt told every program to "generate a service interface (e.g. IDateService)" for
each CALL it made, and specified neither the name nor the shape. Seven files declared
IBdsmfjlService for the same module, and the three examined declared three different
methods on it: ExecuteAsync, ReportAsync and ErrorCodemeldAsync. Each caller had formed
its own idea of what calling that program means.

While every program had its own package this only duplicated. Once a service shares one
namespace it stops compiling, which is how it came to light.

A COBOL program has exactly one entry point, so its interface has exactly one method and
both are derivable. The registry names them, and names a single program responsible for
declaring each: the callee itself when it is in the source drop, since it knows its own
contract, and otherwise the first of its callers in a stable order. Arbitrary, but the
same arbitrary answer on every run and for every caller, which is what stops the
duplicate. Everyone else is told to reference and not declare.

Worth noting where this does and does not bite: across the five bd programs there are six
call targets with one caller each, so the collisions in that estate come from the
copybook conversions rather than from the programs. Those are still reported by the
collision detector; this removes the cause for the program-to-program case.

Tests: 13 cases — the callee declaring its own interface, an external target assigned to
one caller, stability across runs, exactly one declarer per interface, a caller told to
reference but not declare, recursion, a commented-out CALL, and name derivation.
Toolchain suite 348 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
… assumed

doctor.sh has always appended --programs to the reverse-engineer command, and that
command never declared the option, so "./doctor.sh reverse-eng --program X" did not
analyse X — it failed on an unrecognised argument before doing any work. The selector
was added to conversion and not to the analysis that precedes it.

Reverse engineering is subject to the same reason conversion is. A full estate run costs
hours, and a change to a prompt or a model is judged on one program long before it is
trusted on sixty.

The option is declared where the root command declares its own and parsed by the same
SplitPrograms helper, so the two commands accept identical syntax and the selection
reaches the same choke point in FileHelper.

Toolchain suite 348 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Each run wrote to output/java or output/csharp, so a second run silently overwrote the
first. Two runs could not be compared, a demo could not be kept, and a partial re-run
left the previous run's files beside the new ones with nothing to say which belonged to
which.

Runs are now written to a folder named for when they happened — output/csharp/20260917-140000
— which sorts chronologically as text and reads as a date.

That only works if a reader can still find the current output without being told the
stamp, because the portal starts independently of any conversion. The language folder
stays the stable root and the newest run beneath it is resolved on read, so nothing needs
to be passed between them. Two details matter there: a directory is only treated as a run
if its name has the shape of a stamp, so a generated package folder such as Modernized is
never mistaken for one; and a run that failed before writing its report is skipped rather
than shadowing the last good one. Output produced before this change sits flat in the
language folder and is still found.

Tests: 14 cases on the resolver — chronological sorting, newest wins, flat output,
non-stamp directories, a run missing its report, languages not seeing each other's runs,
and stamp recognition by shape. Three more on the parity reader, which is the thing that
would otherwise have quietly reported no data the moment runs became dated. Toolchain
362 passed, portal 78 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The root namespace is assigned in a block this repository injects, but the line telling
the model to use it lives in the User section of the converter prompt — the one section
Prompt Studio rewrites. An AI rewrite can drop that line and nothing fails: the block
still arrives, nothing insists on it, and the model returns to inventing com.example or
CobolMigration. The output still compiles and still looks ordinary. It is simply no
longer one service, and the only way to find out is to read it.

That is the same shape as the placeholder loss PromptPlaceholdersTests exists for. Prompt
wording cannot be relied on to survive editing, so the output is checked instead — a check
is indifferent to how the prompt was worded.

Containment is tested on a segment boundary, so a neighbouring root such as
"Modernizedx" cannot pass for "Modernized". A file declaring no namespace at all is
reported rather than skipped, since that is the same failure with less evidence.

TARGET_ROOT_NAMESPACE is also documented in ai-config.env.example, beside the other
conversion settings, with what it does and what happens beneath it. load-config.sh
exports any variable it finds, so no loader change is needed; the variable was previously
readable only from the source.

Tests: 17 cases — code in the assigned root, an invented root, no namespace at all, Java
packages, segment-boundary containment across six inputs, casing, recursion, non-source
files, a missing directory, and the report text. Verified against the 117 generated files
of the last conversion: none outside the assigned root. Toolchain suite 379 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The portal does not have to run from inside the repository it describes. It is routinely
started from a separate checkout with REPO_ROOT pointing at the one holding source/ and
output/, and the modernization readers honour that.

Five endpoints did not. They resolved their files relative to the portal's own content
root, so they looked for the estate's output inside the portal's directory. A
reverse-engineering report generated minutes earlier was reported as not existing, and
the message helpfully suggested running reverse engineering — which had just been run.

All five now resolve through one definition, which also replaces the private copy inside
ConversionParityReader. Two parts of the same page cannot disagree about where the
repository is if they ask the same function.

Tests: five cases — the configured root, a path resolving into it rather than into the
caller's directory, a configured root that does not exist, walking up to find doctor.sh,
and having nothing to find. Portal suite 83 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
TARGET_ROOT_NAMESPACE was documented in ai-config.env.example and exported by the generic
loader, but load-config.sh never mentioned it. Someone looking for how the generated
layout is configured looks at the loader and its summary, finds Source Folder and Output
Folder, and concludes the setting does not exist.

It is now printed beside them, with its per-language default shown when unset — which is
the case worth naming, because an unset value is easy to miss when the question being
asked is why the output is scattered across five roots.

Verified end to end that the generic exporter carries it: a local config containing
TARGET_ROOT_NAMESPACE="acme.core" reaches the environment with that value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The root was configurable; the structure beneath it was not. ForProgram always returned
root.service and ForSharedTypes always root.shared, both through a Join that could only
ever produce two levels. An estate whose house style is DDD had no way in — there was no
third level to put a layer in, and "shared" could not become "domain".

The layout is now a template the configuration selects. TARGET_ARCHITECTURE takes
"service" (the existing layout, and the default), "ddd" or "layered" for estates that
separate domain records from the programs acting on them, "flat" for an estate too small
to divide, or "custom" with TARGET_NAMESPACE_TEMPLATE for a house convention —
placeholders {root}, {service} and {shared}, anything else a literal segment.

An unrecognised architecture falls back to the default rather than failing a conversion
that is otherwise fine.

ai-config.env.example now carries worked examples rather than a description: the
namespace each architecture produces for source/bd/BDSDA23.cbl, in both languages, plus
three custom templates. The setting was previously documented in prose that never showed
what it did.

One bug found while writing it: substituting placeholders and then casing the result
re-cased already-cased values, turning MyService into Myservice. Each segment is now
cased exactly once.

Tests: 11 new cases — the default unchanged, ddd and layered in both languages, flat,
a custom template, custom with no template, an unrecognised name, a configured root
surviving every layout, and shared types staying separate under ddd. The 16 existing
cases pass untouched, so an unset TARGET_ARCHITECTURE produces byte-identical output.
Toolchain suite 390 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Helpers/RektPromptInjector.cs
Comment thread Helpers/CallTargetRegistry.cs Fixed
Comment thread Helpers/ConversionNamespacePolicy.cs Fixed
Comment thread Helpers/CallTargetRegistry.cs Fixed
Comment thread Helpers/ConversionOutputFolder.cs Fixed
Comment thread Helpers/ConversionOutputFolder.cs Fixed
Comment thread Helpers/ConversionOutputFolder.cs Fixed
Comment thread Helpers/ConversionOutputFolder.cs Fixed
Comment thread McpChatWeb/Services/RepositoryRoot.cs Fixed
Discovery happens three times in this pipeline and only one of them reads the parser's
output. The converters get REKT context; the dependency mapper matches COPY, EXEC SQL and
READ/WRITE with hand-written regular expressions, and the analyzer spends an LLM call per
file describing structure REKT has already recorded.

A regex cannot see what a parser sees. It misses a COPY nested inside a copybook, and it
records the word EXEC where the parse records the real SQL verb — which is the mistake
program-facts schema 1 made and schema 2 exists to correct. Meanwhile the same estate has
17,884 AST nodes, 24,666 control-flow edges and 14,314 data structures already extracted.

Worse than either is having three mechanisms that never compare answers. That is the
failure this estate keeps producing: two sources disagree, nothing notices, and the
reassuring one is believed.

The parse now answers where it can and the text scan is the fallback, not the default.
The fallback is passed as a function rather than called first, so it is not paid for on
programs the parse already covers.

It is deliberately not authoritative everywhere. Where a copybook was missing the parser
was handed a synthesised stub, so its structural claims are inference with better
presentation; below full confidence this yields and the existing extraction runs
unchanged. On one estate that is 35 programs of 41, so the fallback matters. Which source
answered is counted and reported, because an unattributed third mechanism would be
indistinguishable from the two it was meant to reconcile.

Tests: 14 cases — a full parse answering without the scan being consulted at all, each
degraded confidence yielding, no parse output, an empty result from a full parse being an
answer rather than a gap, callees and tables, de-duplication, caching, a path where a
basename is expected, no parse output at all, and the run summary. Toolchain suite 404
passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Agents/DependencyMapperAgent.cs Fixed
Comment thread Agents/DependencyMapperAgent.cs Fixed
Comment thread Agents/DependencyMapperAgent.cs Fixed
Ten findings, seven acted on and three answered.

Path.Combine becomes Path.Join wherever every segment is a relative name this code
builds — the resolver's language root and run folders, and the walk to the repository
root in the dependency mapper. Combine's argument-dropping can only hide a mistake there.

Two loops that projected one sequence into another are expressed as queries: the
call-target contracts, and the namespace template render.

The generic catch in the prompt injector stays, and now says why. It is the outermost of
three, and everything the method does is additive context for a prompt: a conversion
without that context is weaker, a conversion that throws while assembling it produces
nothing at all. Narrowing the type would trade a degraded run for a failed one.

The Path.Combine in CallTargetRegistryHolder also stays. Its second argument is the
source folder, which may be absolute, and an absolute source folder is meant to win
outright rather than be appended to the repository root — the same reason the calls in
RektContextLoader, StubCopybookCatalog and RektPromptInjector keep it.

Toolchain suite 404 passed, portal 113 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The scan accumulated callers by nesting a loop over regex matches inside a loop over
programs and mutating a dictionary of sets from the inside. What it produces is simply
every (callee, caller) pair the source states, grouped by callee, which the query now
says directly.

The self-call guard keeps its reason: recursion needs no interface, and handing a program
one for itself would have it declare and inject a service that is the class being
written.

Toolchain suite 404 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Helpers/RektPromptInjector.cs Fixed
Comment thread McpChatWeb/Services/RepositoryRoot.cs Fixed
Comment thread McpChatWeb/Services/RepositoryRoot.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/ConversionParityReaderTests.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/ConversionParityReaderTests.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/ConversionParityReaderTests.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/RepositoryRootTests.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/RepositoryRootTests.cs Fixed
Eight more Path.Combine findings, all in code added by this branch. Combine exists to let
an absolute later argument replace everything before it. Where every segment is a
relative name the code itself supplies, that behaviour can only ever hide a mistake, so
Join is both safer and more honest about the intent.

Two of these mattered beyond tidiness. RepositoryRoot.PathTo takes caller-supplied
segments and its result is opened and read, so an absolute segment silently discarding
the repository root is precisely the containment failure RepositoryPath was written to
prevent. The walk looking for doctor.sh is the same shape.

The remaining Combine in RektPromptInjector stays. Its second argument is
COBOL_SOURCE_FOLDER, which may legitimately be absolute, and an absolute source folder is
meant to win outright.

Toolchain suite 404 passed, portal 113 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment thread Agents/Infrastructure/RektDiscovery.cs Fixed
Comment thread Helpers/CallTargetRegistry.cs Fixed
Comment thread McpChatWeb.Tests/Modernization/RepositoryRootTests.cs Fixed
Two calls kept Path.Combine because COBOL_SOURCE_FOLDER may be absolute, and an absolute
source folder is meant to replace the repository root rather than be appended to it. That
was true, but it left the rule in a comment and the behaviour in an overload's fine print,
which is why the same finding kept being raised against it.

The test is now written out. Identical behaviour, and a reader no longer has to know how
Path.Combine treats a rooted second argument to see what was intended.

It matters more in the registry holder, where the value is a cache key: a path that
exists nowhere produces a silent miss rather than an error, which is the worst place to
leave an implicit rule.

Toolchain suite 404 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Three empty catch blocks, and in two of them the swallowed failure had consequences
beyond the line it occurred on.

An unreadable facts file and an absent one produce the same result — the program falls
back to a text scan — but not the same cause. Absent means the program was never parsed;
unreadable means it was and the answer was lost. The count is now reported with the rest
of the discovery summary, so a run that quietly stopped using the parse says so.

An unreadable source file in the call-target scan contributes no CALL edges, so a callee
that only it calls can be assigned a different declaring program than it would otherwise
have been. That is not a local effect and should not be silent.

The third is test cleanup: a directory the operating system still holds open is not a
test failure, but silence means a leak is never noticed either, so the reason goes to
stderr — matching the other fixtures on this branch.

No empty catch blocks remain in anything this branch touches.

Toolchain suite 404 passed, portal 113 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The shared-copybook rule told every file "do NOT define these types, reference them from
the shared namespace" and told no file to define them. Each file still needs the type in
order to compile, so each defined it. Measured on the converted bd estate, Bdsdatoi was
declared by nine files: its own conversion and eight that copy it.

The model was not ignoring the instruction. The instruction was unsatisfiable.

A copybook in the source drop is converted in its own right — BDSDATOI.cpy becomes
Bdsdatoi.cs — so an owner already exists and only has to be named. Every file is now told
which types it owns and which it must reference, and exactly one file is told it owns
each. On the bd estate that assigns owners for all 65 copybooks with none left ambiguous.

This is the correction the call-target contracts already needed: an instruction to
reference is only coherent alongside an instruction to declare. Both halves now come from
the same place rather than one being implied.

A copybook absent from the drop gets no owner. Naming an arbitrary user as owner would
put a layout nobody can see into a type everybody depends on; the missing-copybook
reporting already accounts for those.

Copybook-to-copybook use is covered as well as program-to-copybook, because a copybook
that copies another produces a file that would otherwise declare the type again — which
is where most of the measured duplication came from.

Tests: 12 cases — ownership and type-name derivation, every user told to reference while
only the owner declares, copybook-to-copybook use, a missing copybook, an uninvolved
file, a commented-out COPY, self-reference, the shared namespace, and exactly one owner
per type. Toolchain suite 416 passed, portal 113 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
The call-target registry read programs only. A copybook containing a CALL becomes a
converted file in its own right, and that file invented its own interface for the callee
because nothing told it one already existed. Six files declared IBdsmfjlService in the
measured output, and the earlier contract work moved that number not at all — the files
declaring it were copybook conversions, which never received the contract block.

Copybooks are now read as callers. They are not read as call targets: a CALL names a
program, so the declaring file is still the callee's own conversion where it exists.

Tests: two cases — a copybook counted among a callee's callers, and a copybook caller
told to reference rather than declare. Toolchain suite 418 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c7a5d0b7-46c7-4cfb-93b9-f06865dab06e
Comment on lines +63 to +69
foreach (var user in new[] { "BDSDA11", "BDSDA12", "BDSDA23" })
{
var block = registry.ToPromptBlock(user, "C#");
block.Should().Contain("do NOT declare them");
block.Should().Contain("declared by the conversion of BDSDATOI");
block.Should().NotContain("You are the only file that declares it");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant