-
Notifications
You must be signed in to change notification settings - Fork 0
fix(sdk): read helper references as syntax, and drop the surface peer override #572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5357164
fix(sdk): read helper references as syntax, and drop the surface peer…
khaliqgant f903a87
fix(sdk): lex templates and regex literals in the helper scan
khaliqgant 7019a6a
refactor(sdk): parse the flow body instead of lexing it by hand
khaliqgant 70dc7a5
fix(sdk): unescape the context parameter and require a whole-body parse
khaliqgant dc51fcd
fix(sdk): recognise generator method bodies as a context parameter shape
khaliqgant 471e5ba
test(sdk): cover the flowRequirements parameter path, not just preflight
khaliqgant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { parse, parseExpressionAt } from 'acorn'; | ||
|
|
||
| /** | ||
| * Which `f.<namespace>` helpers a flow body actually USES. | ||
| * | ||
| * Read from a parse, not from the text. A helper merely NAMED in a string, a | ||
| * comment, a template quasi or a regex is not used, and declaring a | ||
| * requirement from one refuses a flow for a mount it never touches: | ||
| * `f.run("echo f.gitlab")`, an agent prompt naming `f.slack`, or a PR title in | ||
| * a commit message were each enough to demand a GitLab mount. | ||
| * | ||
| * This was first written as a lexer that blanked comments, strings and | ||
| * regexes. That is the wrong tool: `/` is a regex or a division depending on | ||
| * whether the preceding token ends an expression, which is not decidable | ||
| * without parse context, and the two shapes that ambiguity broke — | ||
| * interpolations (`${await f.gitlab…}`) and a quote inside a regex (`/'/`) — | ||
| * both HID real helper use, deploying a flow without its mount to fail at | ||
| * runtime. A parser settles every one of those by construction. | ||
| * | ||
| * What a parser still cannot see is indirection: `const p = 'gitlab'; | ||
| * f[p].issues…` is invisible to any static reading, because it needs value | ||
| * tracking. This scan is therefore a convenience for the literal case, never | ||
| * the authority — `header.tools[namespace] === true` is the declaration that | ||
| * is, and it short-circuits this entirely. | ||
| */ | ||
| export function helperNamespacesUsed(body: string, root: string): ReadonlySet<string> { | ||
| const program = parseFlowBody(body); | ||
| if (program === null) return textFallback(body, root); | ||
| const used = new Set<string>(); | ||
| walk(program, (node) => { | ||
| if (node.type !== 'MemberExpression') return; | ||
| const object = node.object as AstNode | undefined; | ||
| if (object?.type !== 'Identifier' || object.name !== root) return; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| const namespace = memberName(node); | ||
| if (namespace !== undefined) used.add(namespace); | ||
| }); | ||
| return used; | ||
| } | ||
|
|
||
| /** `f.slack`, `f["slack"]`, `f?.slack` — but not `f[variable]`, which is unknowable. */ | ||
| function memberName(node: AstNode): string | undefined { | ||
| const property = node.property as AstNode | undefined; | ||
| if (property === undefined) return undefined; | ||
| if (node.computed !== true) return property.type === 'Identifier' ? property.name : undefined; | ||
| return property.type === 'Literal' && typeof property.value === 'string' ? property.value : undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Parse whatever `Function.prototype.toString()` produced. | ||
| * | ||
| * It can be an arrow, a function expression, an async method shorthand from an | ||
| * object literal, or a declaration — only some of which are expressions, so | ||
| * each shape gets a try. Types are already stripped by the time a body reaches | ||
| * here (Node strips before evaluating, and bundlers transpile), so this is | ||
| * plain JavaScript. | ||
| */ | ||
| function parseFlowBody(body: string): AstNode | null { | ||
| const options = { ecmaVersion: 'latest' as const, allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true }; | ||
| // `parseExpressionAt` stops at the end of the first expression and does not | ||
| // object to what follows, so `async post(f) { … }` parses as the identifier | ||
| // `async` and reports success having read three characters. Every attempt | ||
| // must therefore consume the whole source, or a method body would be walked | ||
| // as its own name and declare no helpers at all. | ||
| const whole = (source: string, node: AstNode): AstNode => { | ||
| const end = typeof node.end === 'number' ? node.end : -1; | ||
| if (end < 0 || source.slice(end).trim() !== '') throw new SyntaxError('unconsumed input'); | ||
| return node; | ||
| }; | ||
| for (const attempt of [ | ||
| () => whole(body, parseExpressionAt(body, 0, options) as unknown as AstNode), | ||
| () => whole(`(${body})`, parseExpressionAt(`(${body})`, 0, options) as unknown as AstNode), | ||
| () => parse(body, options) as unknown as AstNode, | ||
| // An object-literal method (`async post(f) { … }`) is neither expression | ||
| // nor statement on its own; it only parses inside an object. | ||
| () => whole(`({${body}})`, parseExpressionAt(`({${body}})`, 0, options) as unknown as AstNode), | ||
| ]) { | ||
| try { | ||
| return attempt(); | ||
| } catch { | ||
| continue; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Last resort when nothing parses: match the text. | ||
| * | ||
| * Deliberately the permissive direction. An unparseable body is a shape this | ||
| * code does not understand, and under-reporting would deploy a flow without a | ||
| * mount it needs and fail at the call; over-reporting only asks for a mount | ||
| * that may go unused. Reaching here at all is a bug worth hearing about. | ||
| */ | ||
| function textFallback(body: string, root: string): ReadonlySet<string> { | ||
| const used = new Set<string>(); | ||
| const escaped = root.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); | ||
| const pattern = new RegExp(`(?:^|[^\\w$.])${escaped}\\s*(?:\\.\\s*([\\w$]+)|\\[\\s*['"]([^'"]+)['"]\\s*\\])`, 'gu'); | ||
| for (const match of body.matchAll(pattern)) { | ||
| const namespace = match[1] ?? match[2]; | ||
| if (namespace !== undefined) used.add(namespace); | ||
| } | ||
| return used; | ||
| } | ||
|
|
||
| type AstNode = { | ||
| type: string; | ||
| end?: number; | ||
| name?: string; | ||
| value?: unknown; | ||
| computed?: boolean; | ||
| [key: string]: unknown; | ||
| }; | ||
|
|
||
| /** Depth-first over every child node, without pulling in a second package. */ | ||
| function walk(node: AstNode, visit: (node: AstNode) => void): void { | ||
| visit(node); | ||
| for (const key of Object.keys(node)) { | ||
| if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue; | ||
| const child = node[key]; | ||
| if (Array.isArray(child)) { | ||
| for (const entry of child) if (isNode(entry)) walk(entry, visit); | ||
| } else if (isNode(child)) { | ||
| walk(child, visit); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function isNode(value: unknown): value is AstNode { | ||
| return typeof value === 'object' && value !== null && typeof (value as { type?: unknown }).type === 'string'; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The regex change itself is correct (generator-method bodies now yield the parameter; all other shapes are unchanged), but the new regression test does not cover this code path. The test in helper-reference.test.ts drives
preflightHelpersthroughrefusals, while this line lives inflowRequirements, which has its own separate parameter-extraction path — no test passes a generator-method body toflowRequirements, so reverting this line would still leave the suite green. Add aflowRequirementstest with a*gen(f)method body asserting the helper integration is declared.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid, and the best catch on this PR — fixed in
471e5ba4.I confirmed it by mutation rather than by reading: reverting only the
flow-requirements.tsregex left all 34 tests green, so that mirror was pinned by nothing. My test drovepreflightHelpersthroughrefusals;flowRequirementsextracts the context parameter on its own path, so the two can drift silently.Added a
flowRequirementscase per body shape — async method, plain method, generator method,$-prefixed parameter, plus a string mention that must NOT declare. Re-ran the same mutation with it in place: the revert now fails that test and passes once restored, so the coverage is real rather than decorative.Worth noting for the file as a whole:
preflightHelpersandflowRequirementshaving separate parameter extraction is the same duplication that let the original string-scanning bug exist in two places. The detection itself is now shared inhelper-reference.ts; the parameter regex is the last mirrored piece, and folding it in there too would be the tidier follow-up.