Skip to content

References v3: function registry (Function3/factory) + first-pass FilesReferenceFinder3 - #214

Merged
dk107dk merged 3 commits into
mainfrom
feature/references-v3-function-registry
Jul 31, 2026
Merged

References v3: function registry (Function3/factory) + first-pass FilesReferenceFinder3#214
dk107dk merged 3 commits into
mainfrom
feature/references-v3-function-registry

Conversation

@dk107dk

@dk107dk dk107dk commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Function3 base class (csvpath/references/functions/function_3.py): real, behavior-having reference functions, distinct from FunctionCall3 (the transformer's raw parsed name+arg shape). Declarative per-subclass metadata (SUMMARY, ROLE, DATATYPES, ARG_TYPES, ARG_REQUIRED) instead of porting Args/ArgSet — that solves multi-arg-overload problems these ≤1-arg functions don't have. check_valid() validates generically from that metadata; describe() feeds a future type-ahead registry.
  • ReferenceFunctionFactory: name-keyed registry. build() compiles a FunctionCall3 into a validated Function3, recursing into a nested function arg first. build_chain() compiles a whole function chain and enforces "at most one pointer function per chain, per nesting level" — a pointer nested inside another function's own argument does not count toward its outer chain's budget.
  • Concrete functions: First3/Last3 (POINTER, no arg), Index3 (POINTER, required int arg, 0-based — the spec doc's own "index(7) means the seventh file" wording was a documentation mistake, corrected to "eighth file"), Name3 (CONTEXT_SETTER, required str arg).
  • Name3 exists because of a real discovery made via direct testing: a literal dotted filename ("zero.csv") cannot be written as a bare name_one path segment — PATH_SEGMENT's charset excludes ., and . is the grammar's own name_one/name_three separator, so it fails to parse past that point, not just parses wrong. v1/v2 solved this with a lossy "swap . for _" convention; we used :name("...") instead (a quoted STRING already handles a literal . with zero ambiguity, and avoids the collision risk of the underscore approach).
  • FilesReferenceFinder3: first concrete finder, grounded in the real on-disk manifest schema (confirmed against FileManager/FileRegistrar and a real manifest.json fixture, not assumed). name_one narrows to which file (literal segments, *, or :name("...")); name_three must reduce, via build_chain(), to exactly one pointer function that narrows which version — matching the STRUCTURE table's division of labor. Deliberately out of scope for this pass (each raises a clear ReferenceException3): root_major == "*", the #worksheet marker, function-valued name_one segments other than :name(), and a literal name_three body.

Test plan

  • tests/references/ — 286 tests, all passing
  • Full suite — 1866 passed, 11 failed (known SFTP/S3/remote-backend baseline, unrelated to this change)
  • FilesReferenceFinder3 verified directly against the spec's own EXAMPLE SCENARIO in "creating references v3.txt" ($alpha.files.*.:last() resolves to the exact file the spec says it should)

David Kershaw added 3 commits July 29, 2026 18:33
…/last

Function3 (csvpath/references/functions/function_3.py): base class for
real, behavior-having reference functions, distinct from FunctionCall3
(the parsed name+arg shape the transformer builds). Declarative
metadata per subclass (SUMMARY, ROLE, DATATYPES, ARG_TYPES,
ARG_REQUIRED) instead of Args/ArgSet -- that machinery solves multi-arg
overload problems these functions do not have, since references-v3
functions take at most 1 arg. check_valid() validates arg presence/
type generically from that metadata, recursing into a nested Function3
arg, mirroring matchable.py's parse-time structural-check pattern.
describe() returns the metadata as a dict, feeding a future type-ahead
registry layer.

ReferenceFunctionFactory: name-keyed registry, conceptually similar to
csvpath.matching.functions.function_factory.FunctionFactory per
"requirements for functions.txt". build() compiles one FunctionCall3
into a validated Function3, recursing first if its arg is itself a
FunctionCall3. build_chain() compiles a whole function chain and
enforces "at most one pointer function per chain" -- a pointer nested
inside another function's argument (already compiled by build()) does
not count toward its own outer chain's budget.

First3/Last3: the first two concrete functions, both POINTER role,
arrival/registration order, no arg. Chosen as the smallest
unambiguous starting set; Index3 held back pending a spec question
about 0- vs 1-based indexing.

256 tests in tests/references/ now (26 new), full suite still at the
known 11-failure (SFTP/S3) baseline.
David confirmed the spec's own "index(7) means the seventh file"
wording was a documentation mistake -- indexing is 0-based, so
index(7) is actually the eighth file. Fixed the wording in
"creating references v3.txt" (untracked notes) to match.

Index3: POINTER role, required int arg, registered in
ReferenceFunctionFactory alongside first/last. 7 new tests (263 total
in tests/references/), full suite still at the known 11-failure
(SFTP/S3) baseline.
Discovered via direct testing (not assumed) that a literal dotted
filename like "zero.csv" cannot be written as a bare name_one path
segment: PATH_SEGMENT's charset excludes ".", and "." is the grammar's
own name_one/name_three separator, so the string doesn't just parse
wrong, it fails to parse past that point. v1/v2 solved this with a
lossy "swap . for _" convention (files_reference_finder_2.py's
_starts_with); David and I agreed that is worth avoiding in v3 (it
had a real collision risk, e.g. "test_data.csv" and "test.data.csv"
both normalizing to the same thing) in favor of using the grammar's
own STRING mechanism, which already allows a literal "." with zero
ambiguity.

Name3 (:name("...")): CONTEXT_SETTER role, str arg only for this pass
(the grammar also allows "*"/"@var"/regex there, but those need
machinery -- wildcard-as-arg semantics, runtime variable lookup --
this pass does not need). Used only as a name_one path segment, not in
name_three, matching the STRUCTURE table's division of labor.

FilesReferenceFinder3: first concrete finder, grounded in the real
on-disk manifest schema (one flat, append-only manifest.json per
named-file; "file_home" shared by every version of the same logical
file; arrival order is manifest array order, not sorted by "time" --
all confirmed against FileManager/FileRegistrar and a real fixture,
not assumed). name_one narrows to a file (literal segments, "*", or
:name("...")); name_three must reduce, via
ReferenceFunctionFactory.build_chain(), to exactly one pointer
function (:first()/:last()/:index(n)) that picks the version. Root
major as "*" (every named-file), the "#worksheet" marker, function-
valued path segments other than :name(), and a literal name_three body
are all explicitly out of scope for this pass and raise a clear
ReferenceException3 rather than silently doing the wrong thing.

30 new tests (286 total in tests/references/), including a direct
reproduction of the spec's own EXAMPLE SCENARIO. Full suite still at
the known 11-failure (SFTP/S3) baseline.
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