Imported from restoresrl/pb-ai-code (
skills/pb-context-build/SKILL.md). Install upstream withnpx skills add restoresrl/pb-ai-code --skill pb-context-build. Copyright stays with the author.
Building a scoped context pack for review / refactoring
Use this skill any time you are about to work on PowerBuilder code
in a real codebase (not a fresh empty .pbl) and need to load
enough — but not too much — context.
PowerBuilder PBLs are physical containers, not logical modules. Real legacy apps hold thousands of entries spread across dozens of PBLs. Reading them all at once is impossible; reading the single target entry in isolation misses parents and callers. This skill bridges the gap: it explores the dependency neighborhood of a chosen target, respects a budget, and returns a structured context pack for the work downstream.
When to invoke this skill
- The user asks for code review, refactoring, bug-fix, or extension on an existing PowerBuilder workspace.
- The
pb-reviewflow invokes you as its context-building step. - You are about to export more than one entry and the choice of which entries to export is not obvious.
- You need broad source context before making a change. If the question is
specifically what can break, use
pb-impact-analysisinstead. It runs a focused caller, hierarchy and dynamic-use analysis.
If the workspace is a brand-new .pbl you just created and you only
need to scaffold a fresh entry, this skill is overkill — go straight
to pb-scaffold.
The MCP primitives this skill orchestrates
All from pb-orca-mcp.
This skill never replaces a primitive; it sequences them.
| Primitive | Purpose | Session | Current app |
|---|---|---|---|
pb_workspace_info(lib_path) |
Project shape, projection directory, source encoding, git root, outside_source_tree, source_protection |
no — and no PB install either | no |
pb_target_info(path) |
Parse a .pbt (or .pbw) into liblist + app metadata |
no | no |
pb_library_directory(lib_path, entry_type?) |
List entries in a PBL, optionally filter by type | yes | no |
pb_object_query_hierarchy(lib_path, entry_name, entry_type) |
Inheritance chain (ancestors) of an entry | yes | yes |
pb_object_query_reference(lib_path, entry_name, entry_type) |
Outgoing refs of an entry (callees, ancestors used, types declared, windows opened). ref_type ∈ {simple, open} |
yes | yes |
pb_library_entry_information(lib_path, entry_name, entry_type) |
Metadata for an entry (timestamps, size, base class, comment) | yes | no |
pb_library_entry_export(lib_path, entry_name, entry_type) |
Source body of one entry, as a string in the response | yes | no |
pb_library_export_sources(lib_path, dest_dir) |
Export every entry to an explicit scratch directory outside the project | yes | no |
Two prerequisites, not one. The last column exists because ORCA
distinguishes them and reports them separately: a session that is open
with a library list but no current application answers
PBORCA_LIBLISTNOTSET (-12) or PBORCA_CURRAPPLNOTSET (-13), not
"entry not found". The two query primitives are the ones that need the
full bring-up; everything else works as soon as the session is up. If a
pb_object_query_* call comes back with -13, nothing is wrong with
your arguments — pb_set_current_application has not run yet.
Direction note. ORCA's PBORCA_ObjectQueryReference returns
outgoing refs of the queried object — what it calls and uses, not
what uses it. The opposite direction (incoming refs, "who calls
this") is not exposed natively by ORCA. Reconstructing it means
inverting the index: iterate every candidate caller in the library
list, query each, and keep those whose result set contains the
target. Costly (O(N) on liblist size), so it is offered only as an
opt-in pass — see Caller discovery.
Only the first two work before pb_session_open. Everything else,
pb_library_directory included, fails with a state guard until the
session is up.
"Nothing to report" arrives in two different shapes, and one of them
looks like a failure. Both query tools can answer "empty" with an
error envelope instead of an empty list:
| Response | Means |
|---|---|
{references: [], count: 0} |
no outgoing refs |
PBORCA_OBJHASNOREFS (-15) |
no outgoing refs — same thing |
PBORCA_OBJHASNOANCS (-14) |
no ancestors |
Which shape you get is not predictable from the entry type: in one
library a window with no refs returned the empty list while a datawindow
with no refs returned -15. And -14 is the normal answer for any
object deriving straight from a built-in class (window,
nonvisualobject, …), which in most codebases is a large fraction of
them.
So treat -14 and -15 as empty, not broken. Record "no ancestors"
or "no outgoing refs" in the pack and move on. Do not report them to the
user as errors, do not retry them, and do not let them abort the walk.
With one check first, because -15 says two different things. ORCA
answers out of the reference information stored in the library, and that
information is written when an entry is compiled. An entry that has
never been regenerated has none — so ORCA reports no outgoing references
for an object whose source plainly contains Open(w_main). Same answer,
opposite meaning: not "this calls nothing" but "nobody asked me". Move on
from that one and the pack carries an empty dependency map that looks
like a finished answer, which is the worst shape a wrong answer can
take here — the review that follows will conclude the entry is isolated
and reason from it.
Two discriminators, both cheap:
- Per entry: the source is already exported. If it contains a call,
an
Open(, an ancestor use — and ORCA returned no references — the data is absent, not empty. - Per library: if every entry answers
-14/-15, that library has almost certainly never been built. One object with no refs is ordinary; twenty in a row is a fact about the library, not about the objects. Measured on a real fixture: an application and a menu both answeredPBORCA_OBJHASNOREFSwhile their sources containedOpen(w_genapp_main)andOpen(w_genapp_about).
When you conclude the data is absent, say so in the pack — a
refs: unavailable (never compiled) marker, not an empty list — and say
it to the user, because it changes what the pack is worth. The repair is
pb_object_regenerate, which rebuilds the compiled form and with it the
reference information.
It is a write. It modifies the .pbl, so it is not yours to run
inside a read-only flow: offer it, name what it changes, and let the user
decide. A review that silently regenerated a customer's library to
improve its own context would be doing exactly what this kit refuses to
do elsewhere.
.pbd libraries are enumerable but not readable, and the error for
that says the opposite. A target's LibList routinely contains
compiled .pbd libraries alongside .pbl ones — a vendored component,
a shared library shipped without source. pb_library_directory works
on them and returns a full listing: names, types, sizes, timestamps.
Every per-entry call on those same names then fails:
| call | on a .pbd |
|---|---|
pb_library_directory |
works — full entry list |
pb_library_entry_export |
PBORCA_OBJNOTFOUND (-3) |
pb_library_entry_information |
PBORCA_OBJNOTFOUND (-3) |
Measured on a translation library whose .pbd listed 24 entries: the
directory named gettext and n_gettext, and exporting either
answered "'gettext' was not found".
The wording is the trap. "Was not found" reads as a misspelled name or
the wrong library, and the honest reaction — check the spelling, then
look in the other libraries — is wasted work, because the name is
right and the library is right. What is missing is the source: a
.pbd is compiled output and carries none.
So: before exporting an entry, look at the extension of the library
it lives in. When a ref resolves into a .pbd, do not try to read
it and do not treat it as a stale reference. Record it in the pack as
present-but-unreadable, with the library that owns it, and say so —
the source lives in whatever project builds that .pbd, which is the
same conversation as a vendored outside_source_tree library.
Which read primitive to use. pb_library_entry_export puts the
body straight into your context and is the right default for the
handful of entries in a pack — with one thing to know: it returns the
body, so no $PBExportHeader$, no $PBExportComments$, and no
binary section. An entry hosting an OLE or ActiveX control carries a
binary tail of serialized control state, and on one measured
olecustomcontrol that was 40% of the file. Dropping it is right for
reading — it is opaque and unreviewable — and wrong for anything that
compares sizes or bytes, where pb_object_export_file to a scratch
directory is the primitive that returns the whole file.
pb_library_export_sources writes a whole library to disk in one call.
Use it when you want to grep across a library rather than read it
(entry callers, Dynamic Call, or uses of a literal), and when per-entry
calls would dominate. Always pass a dest_dir outside the project. The
result is scratch data, never a replacement for the projection that the
PowerBuilder IDE creates and manages.
Do not read its response. It returns one record per entry, each carrying
the full absolute path, so the reply grows with the library and none of it
is content. Measured: 21 entries came back as ~5.5 KB of JSON, about 262
bytes an entry. Extrapolated to the libraries you would actually use it on,
that is ~25 KB for a 96-entry library and ~84 KB for a 327-entry one.
Take count, skipped, and failed from the response, take the dest_dir
you passed, and read the files there.
On a pbl_only project, do not use a bulk export to create ws_objects/.
The developer enables Git or SVN in the PowerBuilder IDE, and PowerBuilder
creates the managed projection.
Step 0 — Ask the workspace what it is
Before any session bring-up, call pb_workspace_info(lib_path) on
one library of the target. One call, no ORCA session, no PB install
required. Four fields change how the rest of the work proceeds:
Then look for libraries nobody told you about. One call answers for
one library, so a flow that calls it once can only confirm what you
already knew — and a vendored dependency is by definition the thing you
did not know was there. After the first call, glob the project root for
*.pbl that are not under the ws_objects_dir the call just handed
you, and call pb_workspace_info on each hit. Sessionless and cheap: a
handful of calls on any real project. List what comes back flagged, in
the pack, even when the answer is none — "I looked and there are none"
and "nobody looked" are different statements and only one of them is
worth writing down.
A library sitting in dep/, vendor/, lib/ or beside the .pbls
without a projection is the shape to expect. Note that it may not be in
any target's LibList, so enumerating the liblist is not a substitute.
-
mode:ws_objectsmeans PowerBuilder maintains a readable.sr*projection beside the.pbl;pbl_onlymeans the library has no managed projection.pb-ai-codereads and searches the projection, but every write acts on the.pblthrough ORCA and lets PowerBuilder synchronize it. The answer is per library, not per project. A vendored.pblinside a projected workspace can still bepbl_only. Do not carry one library's answer to another, and never create a projection for a vendored library. -
source_protection—protected,unprotectedorno_git.unprotectedis a stop-and-say-so, not a footnote. No.gitattributesrule exempts the.sr*files from git's line-ending translation, so git stores them with LF and hands them back as CRLF, so the index and working tree differ from the bytes PowerBuilder wrote. A line-ending-only edit can have an empty diff and disappear after staging without Git ever storing the working bytes. Measure the condition withgit ls-files --eol <projection dir>and effective attributes on the real.sr*paths. Report it, but do not edit the project-owned.gitattributesin this read-only flow.The repair is
*.sr* -text, with*.pbland*.pbdasbinary, thengit add --renormalizescoped to the real projection paths. The working files must remain byte-identical before and after. Use-text, notbinary: both stop translation, butbinarydisables diffs. An SVN checkout needs a separate property check and no Git configuration. -
export_encode— the workspace'sDefaultExportEncode. You never have to act on it (ORCA writes the files), but it belongs in the pack: it is what makes a hand-edited file look out-of-sync to the IDE. There is no field calledencoding: the tool returnsexport_encode,orca_encoding,observed_encodingandencoding_source. Record all four.observed_encoding: nullmeans there was nothing to sample — a library with no projection — and is not a mismatch; skip the check rather than raising a finding about files that do not exist. Otherwise,export_encodedisagreeing withobserved_encodingmeans the workspace is already inconsistent and the IDE will rewrite those files on its next export, which is a finding, not a footnote. -
outside_source_tree— a boolean about the library you asked about, not a list. True for a library that sits inside the project but outside its source tree. This one is load-bearing for a review. A library flagged this way is a vendored dependency snapshot or a third-party component: it gets replaced wholesale by whatever produced it, so a refactoring proposed inside it will be overwritten at the next update of that dependency. Either exclude it from scope, or tell the user plainly that the finding belongs upstream, in the project that owns that library.What the pack does with one, since a warning is not a rule: include its entries as read-only context when the dependency graph reaches them — an ancestor three levels up often lives in one — and mark them in the
## Sourcesheading as— vendored, read-only. They do not count against the entry cap, because they are context and not scope. Never propose a fix inside one, and never generate a projection for one. An unmarked vendored ancestor reads as ordinary project code, and the reviewer files a finding that will be overwritten by the next dependency update. -
work_dir: the server's default scratch location for a library with no projection. Do not rely on it during context building. Pass an explicitdest_diroutside the project so a read-only flow leaves no.pb-orca/directory behind and cannot be mistaken for a project projection.
Record all four at the top of the context pack.
Session bring-up
The session-bound primitives need, in order: pb_session_open
(pb_version or install_path is required — there is no
auto-pick, because .pbt/.pbw files do not record a PB release;
list the options with pb_discover_pb_install and say which you
chose), then pb_set_library_list, then
pb_set_current_application.
Two things to keep in mind:
-
A modified
.pbwis expected noise. Do not report it.pb_set_current_applicationmay rewrite it as a side effect, and so does a developer simply opening the workspace in the PB IDE and selecting a different target — theDefaultTargetandDefaultRemoteTargetlines move on their own, constantly, without anybody deciding anything. Treating that as a finding, or advising a revert, wastes the reader's attention on a file that changes by breathing on it.The one part of that file worth a word is the target list. If the
@targetsblock gained or lost an entry, the set of things the workspace builds has changed, and that is worth raising. Nothing else in there is.git diff -- '*.pbw' | grep -E '^[+-]' \ | grep -vE 'DefaultTarget|DefaultRemoteTarget|^[-+]{3}'Empty output means only the default-target lines moved: say nothing. Non-empty means a target was added or removed: say that, specifically, and do not bury it under a general remark about the
.pbwbeing dirty.The same rule governs the other direction. A local, uncommitted
.pbwedit that has disappeared is not evidence that something went wrong — the IDE reclaims that file routinely. Do not raise it as a possible incident. -
Sessions are not cheap to churn. Open one per unit of work, not one per object.
-
The library list can be set once and only once. A second
pb_set_library_liston the same session fails withPBORCA_DUPOPERATION (-2), "Library list has already been set". To point at a different target — or at a different workspace, which is what a second review in one session usually means — you mustpb_session_closeand open again. There is no way to amend it.Do not shrug this error off, because its wording invites exactly that: "already been set" reads like your call was redundant, and the tempting recovery is to carry on. Carry on and every subsequent
pb_library_directory,pb_object_query_hierarchyandpb_object_query_referenceanswers against the previous library list, silently and plausibly — entries resolve, hierarchies come back, references come back, and all of it describes a workspace you are no longer looking at. Treat-2as "close the session and start over", and re-runpb_set_current_applicationafter you do.
When bring-up fails, pb-orca-mcp check <.pbw|.pbt|.pbl> is a CLI
that validates the whole stack against the real project with no MCP
in the way. Use it as the diagnostic prerequisite instead of
guessing.
Three scope flavors
Pick the smallest one that matches the user's request.
Flavor A — entry-driven (default for "review this object")
Input: one entry triple (lib_path, entry_name, entry_type).
- Export the target entry's source (
pb_library_entry_export). - Get the inheritance chain via
pb_object_query_hierarchy. Export every ancestor up to the budget depth (default 3 levels; the topmost framework class —window,nonvisualobject, … — is the natural stop). - Get the outgoing refs (callees, used types, opened windows)
via
pb_object_query_reference. For each, read its metadata viapb_library_entry_informationto decide whether to expand it. By default expand onlyref_type=open(windows the entry opens — they are the integration boundary downstream); listref_type=simple(functions called, types declared) without expanding unless budget allows. - If budget allows and the user asked for transitive outgoing refs (depth 2+), repeat step 3 on the depth-1 set — but prune aggressively: cap each level's expansion at ~5 entries.
- (Opt-in) If the user asked for callers, run the inversion pass. Off by default because it is O(N) on liblist size.
Flavor B — target-driven (default for "review this .pbt")
Input: a .pbt (or .pbw) path.
pb_target_info(path)→lib_listandapp_name. Cheap, no enumeration.- Immediately show the PBL list to the user, marking any library
that
pb_workspace_inforeported asoutside_source_tree, then ask which PBL or entry-name pattern to focus on. - Only after the user has chosen a sub-scope (a single PBL → Flavor
C; a single entry → Flavor A; a name pattern → filtered Flavor C)
run
pb_library_directoryon that scope.
Why not sweep first? On real legacy targets (~6-12 PBLs × 100+ entries each) enumerating every PBL costs many round-trips and almost always ends in "too big, refine". Skipping the sweep reaches the same refinement turn one step earlier.
Opt-in full sweep: if the user explicitly asks for a complete overview ("give me a count by entry type for the whole target"), enumerate all PBLs, aggregate, and return a summary ("1834 entries total: 1240 functions, 312 userobjects, …; largest PBLs by entry count: …"). Then ask for refinement before any export.
This flavor's job is orientation, not export. Exporting at target level is almost always a budget violation.
Flavor C — PBL-driven (default for "review this PBL")
Input: one lib_path.
pb_library_directory(lib_path, "any")→ list of entries.- Small count (≤ ~20 entries): export all of them in order. This is "review the whole PBL".
- Moderate count (~20-100): filter by
entry_typeif the user named one ("review the userobjects incore.pbl"), then apply step 2 to the filtered set. - Large count (> ~100): refuse to export en masse. Return a summary
and ask for refinement (single entry, or a pattern on entry name).
If what the user actually wants is a search across the library
rather than a read of it, that is the case for
pb_library_export_sourceswith an external scratchdest_dir, plus grep.
Flavor D — intent-driven (default for "review the X flow")
Input: a description with no path in it — "the shipment tracking flow", "error handling in the import layer", "the logger chain". This is how people actually ask, and it is the one form where the target has to be discovered before anything else can happen.
Search content, not names. The tempting move — guess a naming
pattern and enumerate libraries with pb_library_directory — fails on
exactly the codebases this skill exists for, and it fails quietly by
returning something. Measured on one workspace, "the shipment tracking
flow" matched one file by filename (a DataWindow, not the flow) and
sixteen files across five libraries by content. The reason is
ordinary: the domain is spelled in the codebase's own language and its
vendors' names — the flow lived in spedizione, vettore,
n_gsped_client and n_get_shipments — and no pattern guessed from
the user's words reaches those.
- On a
ws_objectsproject, grep the projection. It is on disk, it covers every library at once, it needs no ORCA session and no PB install, and it costs nothing. Search for the domain nouns and their likely synonyms in the codebase's language; ask the user for a term if the first pass is empty rather than guessing a third time. - On a
pbl_onlyproject there is no projection to grep.pb_library_export_sourceswith an explicitdest_diroutside the project gives you the same text without materializing a projection the project deliberately does not have — this is the safe form of the call that is otherwise discouraged for vendored libraries. Fall back topb_library_directoryper library only if even that is refused. - Group the hits by library and by entry, and present that, not a
file list: "16 entries across 5 libraries —
mw_asp(2),mw_io(4),mw_gsped(2),mw_ane(3),mw_rev(5). Which of these is the flow you mean?" A concept that spans five libraries is a fact the user wants to see before choosing, and often it is the finding. - Then hand the confirmed set to Flavor A and walk the hierarchy from there.
Two cautions carry over. The caller-discovery notes on word boundaries and library-list scoping apply to this grep too. And a content search finds comments and strings as well as code, which here is a feature — a comment naming the flow is evidence about where it lives — but it means the hit list is a starting point for the propose-confirm loop, never the scope itself.
Outgoing refs from ORCA (default)
For each exported entry, call pb_object_query_reference to get the
outgoing_refs list — what the entry calls, opens, declares as a
type, or otherwise references. This is the native, exact
direction ORCA exposes and it is essentially free (one call per entry
already in the pack).
Each item comes back with library, entry_name, entry_type and
ref_type (simple for declarative refs, open for runtime window
opens). Record them confidence: high — they come from ORCA's index,
not from parsing.
What ORCA cannot see:
Dynamic Call,Dynamic Function,Dynamic Eventinvocations.- DataWindow expression strings (
SetItem(row, "col", value)where"col"could be anything at runtime). - Function names built at runtime by concatenation:
f_call(name + "_handler").
For those, see the heuristic fallback below.
Caller discovery — opt-in inversion (off by default)
ORCA has no native primitive for "who calls this entry". To compute incoming refs the skill must invert the index: iterate the library list, query each candidate caller, keep those whose result set contains the target.
This is O(N) on liblist size — on a monolith (~6-12 PBLs × 100+ entries each) it can mean 1000+ ORCA calls per target entry. Hence off by default.
Activate it only when the user explicitly asks ("who calls
n_logger.flush?", "find all callers of f_legacy_thing") or when a
downstream skill needs the caller set. When activated:
- Iterate every entry in the configured liblist via
pb_library_directory(lib, "any"), skipping entries already known to be in the call-graph closure. - For each candidate call
pb_object_query_reference(candidate)and check whether the target appears in its outgoing refs. - Cap the work: stop after N candidates probed (default 500) or N matches found (default 20), whichever comes first. Report honestly ("scanned 500/2400 entries, found 12 callers; widening would take roughly 5× longer").
- Mark each caller
confidence: high(it is ORCA — exact) with theref_typefrom the query.
The cheaper alternative worth offering first. If the question is
"who mentions this name", pb_library_export_sources on the candidate
libraries, always with an external scratch dest_dir, plus a grep over the
resulting files answers it in a couple of calls instead of a thousand. Being textual, it also
catches the dynamic invocations ORCA cannot see. Offer it as the fast
pass; reserve the inversion for when exactness matters.
Two things make its false positives worse than "some noise", and both have cheap fixes. Measured against ORCA on one real name, plain grep returned 5 files where 3 were real — a 40% false-positive rate, on a name chosen for being ordinary.
-
PowerBuilder names are compositional, so substring matches are the rule, not the exception. The conventions that make a codebase readable —
<base>_ancfor ancestors,<base>_sqlsrv10for variants,regola_<base>for a related concept — guarantee that short names are prefixes and infixes of long ones. Greppingspedizione_ancmatchedregola_spedizione_anc, a different class, in seven places in one file; ORCA answeredPBORCA_OBJHASNOREFSfor it, so the reference does not exist at all. Anchor on word boundaries —grep -rlE '\bname\b'— which is correct here because_counts as a word character, so\brejectsregola_spedizione_ancwhile keepingspedizione_anc. That one change took the same query from 5 files to 3. -
Grep has no idea what a library is, and names collide. The filesystem search spans every projection directory, while the review is scoped to one target's library list. One workspace had 20 entry names duplicated across libraries, and
u_app— the application object every target defines — existed in 13 of them. Grepping it answers with thirteen unrelated classes, twelve of which are not in the build you resolved to. Restrict the search to the projection directories of the libraries in the resolvedLibList, and say in the pack that you did.
Both errors are over-reporting, which is the direction that costs most: the caller count is what a reviewer sets a finding's blast radius from, and an inflated one turns a dead method into an urgent fix. When a caller count decides a priority, spend the ORCA queries on that one name and say which number came from which method.
For very-base userobjects with hundreds of callers, do not chase the full set: give a count and the top-N by liblist proximity.
Heuristic fallback for dynamic patterns (optional)
Some dependencies are invisible to ORCA's index because they resolve
at runtime. If you suspect a refactor crosses such a boundary, run a
regex pass over the exported source for the patterns above, flag the
candidates confidence: low, kind: dynamic, and present them as
"ORCA cannot confirm these edges; review by hand". Never treat them
as hard topological constraints downstream.
This pass is intentionally narrow: it covers only what ORCA cannot
see. The bulk of the call-graph already comes from
pb_object_query_reference with confidence: high.
Editability contract
The outgoing_refs from ORCA are facts; the heuristic
dynamic-pattern fallback produces proposals. The downstream
plan-file format lets the user edit the depends_on field by hand.
User-added edges are marked confidence: user-augmented and override
anything the heuristic pass suggested. ORCA-sourced edges marked
confidence: high should not be edited away without good reason.
Budget mechanics
Judgment-based, not a strict counter. Defaults that work as starting points on monolithic codebases:
-
Cap on exported entries: the size budget is the rule; 20 entries is a rule of thumb for typical sizes, not a limit to prune to. If the natural flow would exceed it, prune (depth, then breadth) until under cap.
The size cap is the binding one; the count is advisory. Twenty is twenty entries of typical size. A library of global functions is mostly 300-600 byte wrappers, and pruning six of them to satisfy an arithmetic limit removes the callers that give the object under review its contract, while freeing well under 1% of the budget. Entries below ~2 KB do not count against the twenty. If the pack is under the size cap, do not prune to meet the count — say what you included and why.
-
Soft cap on cumulative source size: ~150 KB of plain text (~50 K tokens). Track as you go; if you cross it before reaching the outgoing-refs expansion, stop and report what you have. The target entry and its inheritance chain take precedence.
Neither size field ORCA reports is the size of the export. There are two, they are different numbers, and both mislead:
field where relation to the exported bytes source_sizepb_library_entry_information≈ 2×, always — it counts UTF-16 code units object_sizepb_library_entry_information, and thesizein everypb_library_directoryrowunrelated, and wrong in both directions depending on entry type source_sizeis the safe one: halve it. Measured on a real library,source_size: 41076for an entrypb_object_export_filewrote as 20 577 bytes, andsource_size: 122652for one it wrote as 61 375. The factor held on every entry checked. Not halving it makes the pack look 2× more expensive than it is, which prunes scope that did not need pruning — conservative, but a real distortion at the moment you decide what to leave out.object_sizeis the compiled object, and it is the trap, because it is the only size in apb_library_directorylisting — which is exactly what Flavor C has to budget from, before anything is exported. Measured across one 10-entry library:entry type object_size÷ exported bytesuserobject 2.7× – 7.9× (over) menu 3.7× – 4.0× (over) window 3.5× – 4.6× (over) datawindow 0.58× – 0.67× (under) Compiled PowerScript is bulkier than its source; a DataWindow's source is bulkier than its compiled form. So on a library of mixed types the errors do not even cancel — they point opposite ways, and the type that under-reports is the one whose sources are largest. In that library the directory listing totalled 216 KB against 158 KB of actual source, while the four DataWindows alone — 77% of the real bytes — looked like a third of the total.
So do not budget a library scope from the directory listing. Use it to enumerate and filter, then get
source_sizefor the shortlist and halve it. One extra call per candidate entry, and it is the difference between a budget and a guess. -
Default expansion depth: ancestors = 3, outgoing refs = 1. Both configurable per invocation.
-
Caller discovery (opt-in) caps: max 500 ORCA queries on the liblist, max 20 callers reported, whichever comes first. Always honest about partial scans.
-
Pruning order when over budget: (a) drop
simple-typed outgoing refs first (declarative refs are usually less load-bearing thanopen-typed ones); (a′) when every ref issimple, rank them instead — see below; (b) drop ancestors beyond depth 2; (c) drop framework-level ancestors (nonvisualobject,window,userobject) since the language reference covers them — seeappeon-query; (d) skip the caller-discovery pass entirely.(a′) matters more often than (a). Step (a) assumes the refs are a mix of
simpleandopen, and on anonvisualobjectthey never are:openis a window-opening reference, so a non-visual class has none and step (a) degenerates to drop-everything-or-nothing. Measured on a real persistence base class: 37 outgoing refs, allsimple, zeroopen, with a depth-1 expansion at 159% of the size cap. The type told you nothing about what to cut.Rank by these two, in order, and say in the pack which ones you kept and why:
- Is the entry in the target's own inheritance or delegation chain? An ancestor, an interface the target owns, the datastore it drives — these are the code the target's behaviour is made of, and a review without them is guessing. Keep them.
- Size, descending, among everything left. Two 20 KB peers cost as much as the target itself and usually buy one finding between them; ten 500-byte helpers cost nothing and often carry a contract.
What that produced on the case above: the target plus its two interface classes plus its datastore — 4 entries, 95 KB, 62% of the cap — with the eighteen referenced peers (21 KB, 17 KB, 15 KB, 14 KB and so on) left out and listed by name in
## Skipped, with the questions their absence leaves open. That last part is the obligation: a pruned pack is honest only if the reader can see the shape of the hole.
These caps are starting points, not law. Adjust if the user signals they want a deeper or shallower view.
Output: the context pack shape
Return a structured summary to whoever called you (usually
pb-review, but a direct user invocation is
fine). Recommended shape — flexible markdown, not a rigid schema:
## Context pack: <target description>
**Workspace** (from `pb_workspace_info`): mode=<ws_objects|pbl_only>,
encoding=<export_encode> (orca=<…>, observed=<…>, from=<…>), git=<yes|no>,
outside_source_tree=<the flagged libraries found by the sweep above, or 'none found'>, source_protection=<…>
**Target**: `lib_path` :: `entry_name` (`entry_type`)
**Inheritance chain** (depth: N exported, M skipped):
- ancestor_1 (lib) — exported
- ancestor_2 (lib) — exported
- nonvisualobject — framework, not exported (see appeon-query)
**Outgoing refs** — via `pb_object_query_reference` (callees,
ancestors used, types declared, windows opened), depth 1, N total,
M exported:
- ref_1 (lib, kind=function, ref_type=simple, confidence=high) — exported
- ref_2 (lib, kind=window, ref_type=open, confidence=high) — exported
- ref_3 (lib, kind=userobject, confidence=low, kind=dynamic) — Dynamic Call, heuristic
**Incoming refs (callers)** — OPT-IN, off by default; populated only
when the user asks. Via inversion of `pb_object_query_reference`
over the liblist:
- caller_1 (lib, ref_type=open, confidence=high)
- ... (scanned N/M liblist entries; capped at 20 unless overridden)
**Budget**: N entries exported, ~K tokens of source loaded.
Pruned: <what was dropped and why>.
## Sources
### `lib_path` :: `entry_name` (target)
<full source>
### `lib_path` :: `ancestor_1`
<full source>
The pack is informational, not executable. ORCA-sourced edges are
confidence: high; heuristic dynamic-pattern edges are
confidence: low and belong in the output as warnings, not facts.
Limitations (explicit non-goals)
- Caller discovery is opt-in and capped. Inverting ORCA's index is O(N) on liblist size. The skill is always honest about partial scans.
- Dynamic patterns are flagged, not resolved.
Dynamic Call,Dynamic Function, DW expression strings and runtime-built names are invisible to ORCA's index. The optional heuristic pass marks themconfidence: low; never treat them as hard edges. - No cross-target review. If the workspace has multiple
.pbtfiles and the refactor would span them, this skill does one target at a time. - No caching of context packs between sessions. Every invocation rebuilds from scratch. Caching is a future optimization if review latency becomes painful.
- No automatic bulk sweep. Manual-assist by design: the user picks the target, the skill helps understand it.
Failure modes to handle gracefully
Every tool returns one of exactly two shapes: a success payload, or a
single error envelope ({"error": {"code", "name", "message"}}).
Branch on "error" in response first. Read success payloads
defensively — index by key, do not assume a fixed field set.
- Session not open: the session-bound primitives fail with a state-guard error. Bring the session up rather than retrying.
- Entry not found:
pb_object_query_*errors when the entry is not in the library list. Verify viapb_library_directoryfirst, then guide the user to the right spelling or PBL. Do not confuse this with-14/-15, which mean the entry was found and has nothing to report. - PBL not in library list: even when the file exists on disk,
pb_object_query_*only sees entries in the configured liblist. Check thatpb_set_library_listcovered every relevant PBL. - Wrong architecture / DLL not found: the server's Python must
match
pborc.dll(x86 through PB 2025).pb-orca-mcp doctorreports the whole picture; do not work around it. - Huge incoming-ref set during opt-in caller discovery (a base userobject used everywhere): truncate aggressively and report the count honestly.
Boundaries with sibling skills
pb-impact-analysis: use it when the question is specifically "what breaks if I touch X". It reuses the caller discovery rules here but produces a tighter blast-radius report. This skill loads context for any downstream task.pb-scaffold: unrelated — that one creates new entries; this one helps understand existing ones.appeon-query: for questions about PowerScript syntax or runtime API, not project-specific code. Complementary: this skill loads the project context, that one loads the language context.pb-src-format: once you have the exported source, its wiki pages explain the file format if you need to edit it.pb-reviewis the primary consumer: context-build → review → plan file → apply loop.