Phase 01 — Context gathering: Layer A (Node.js): Best-practices design¶
Lens: Best practices — idiomatic, maintainable, conventional, well-tested.
Designed by: Best-practices design subagent
Date: 2026-05-12
Companions (parallel): design-performance.md, design-security.md
Source of truth for scope: docs/roadmap.md "Phase 1", localv2.md §4–§8, §12, docs/production/design.md §2 (load-bearing commitments), docs/phases/00-bullet-tracer-foundations/final-design.md.
Lens summary¶
Phase 0 planted the spine: the probe contract, async coordinator, content-addressed cache, layered JSON Schema, output sanitizer, subprocess allowlist, audit anchor. Phase 1's job is not to extend that spine but to populate it — five new probes (NodeBuildSystem, NodeManifest, CI, Deployment, TestInventory) alongside the existing LanguageDetection, each in its own module, each declaring its applies_to_languages, applies_to_tasks, and declared_inputs, each owning one schema slice under src/codegenie/schema/probes/, each exhaustively unit-tested against fixture Node.js repos.
The best-practices bet: the Phase 0 contract is the budget, not a starting point. Every Phase 1 component lives in a file that Phase 0 anticipated. No new top-level packages. No "Phase 1 wrapper" layer around the Phase 0 coordinator. No bespoke fixture harness — pytest's tmp_path plus a fixtures/ directory of committed minimal Node.js repos is the entire fixture story. The probe ABC stays byte-for-byte localv2.md §4 (per phase-0 ADR-0007); the snapshot test continues to enforce it.
The deepest commitment in this lens [Rule 11, Rule 8]: conformance over taste. The Phase 0 cache returns ProbeOutput directly via the Ran | CacheHit | Skipped pass-through (final-design §2.6); Phase 1 reads that contract and uses it as-is. The Phase 0 sanitizer two-pass remains the only path to disk; Phase 1 probes hand their ProbeOutput to the coordinator and never touch the writer. The Phase 0 additionalProperties layering (final-design §2.9) is the extension hook for new probe sub-schemas — Phase 1 adds five $refs and five files under schema/probes/, period.
Conventions honored¶
- No LLM in the gather pipeline (ADR-0005,
production/design.md §2.1) → Every Phase 1 probe is pure Python parsing of files on disk (YAML, JSON, lockfile formats) plus, forTestInventory, atree-sitterAST query routed through the existing subprocess allowlist. The Phase 0fenceCI job (final-design §3.2 job 6) continues to assert the dependency closure contains no LLM SDK; Phase 1 adds no LLM SDK, so the fence stays green by default. Thepyproject.tomlextras shape from final-design §2.2 is preserved — no probe importsanthropic,langgraph, or any other agent SDK. - Facts, not judgments (
production/design.md §2.2) →NodeManifestemitsnative_modules: [{name, version, requires_node_gyp, binary_artifacts, system_deps_required}]from a hand-curated catalog. It does not emitsafe_for_distroless: true.CIemitsimage_build_command: "docker build -t ..."; it does not emitci_appropriate_for_migration: true.Deploymentemitssecurity_context.run_as_user: 1000; it does not emitproduction_ready: true. The_ProbeOutputValidator's recursiveJSONValuetype from Phase 0 (final-design §2.3) structurally rejects any field that would carry judgment-shaped data — there is noLiteral["safe", "unsafe"]type in the schema; there are counts, paths, versions, presence flags. - Honest confidence (
production/design.md §2.3) → Every Phase 1 probe reportsconfidence: high | medium | lowand an explicitwarningslist. ANodeBuildSystemrun that finds three lockfiles (a real failure mode in Node monorepos) reportsconfidence: lowandwarnings: ["multiple lockfiles detected: pnpm-lock.yaml, package-lock.json, yarn.lock"]. ACIrun on a repo with no recognized CI provider reportsapplies()→True(the slice still has to exist) withconfidence: low,warnings: ["no recognized CI provider; checked: .github/workflows/, .gitlab-ci.yml, .circleci/, Jenkinsfile"]. No probe overclaims by silence. - Extension by addition (
production/design.md §2.5, ADR-0007 production) → Each Phase 1 probe is one new file undersrc/codegenie/probes/, one new sub-schema undersrc/codegenie/schema/probes/, one new test module undertests/unit/probes/, one new entry in the explicit registry import list insrc/codegenie/probes/__init__.py. Zero edits tocoordinator.py,cache/,output/,audit.py,schema/validator.py,exec.py. The Phase 0test_probe_contract.pysnapshot test fails CI if the ABC drifts; Phase 1 must not trigger it. - Determinism over probabilism (
production/design.md §2.4, ADR-0006) → Parsers are deterministic (json.loads,yaml.safe_load,tomllib,tree_sitter). No LLM, no heuristic fuzzy matching, no probabilistic classifiers. The native-module catalog (src/codegenie/catalogs/native_modules.yaml) is a flat YAML lookup table; expanding it is a data edit, not a code change. - Organizational uniqueness as data, not prompts (
production/design.md §2.6) → The native-module catalog and the CI-provider detection table are both YAML data files loaded at module import. Adding a new native module (e.g.,node-rdkafka) is a one-line YAML PR. Adding a new CI provider (e.g., Buildkite) is a new entry insrc/codegenie/probes/ci_providers.yaml. - Progressive disclosure (
production/design.md §2.7) →repo-context.yamlcontinues to index, not inline. Phase 1 probes write large raw artifacts under.codegenie/context/raw/<probe>.json(the lockfile dump fromNodeManifest, the parsed Jenkinsfile fromCI, the rendered Helm values fromDeployment); the YAML manifest only references them by relative path. - Humans always merge (
production/design.md §2.8, ADR-0009) → N/A in Phase 1 (no PRs opened). - Cost observability (
production/design.md §2.9, ADR-0024) → Per-probe wall-clock continues to be recorded in the audit anchor (final-design §2.12). Phase 1 adds five more entries to that record; the cost ledger of Phase 13 will read this same audit record.
Goals (concrete, measurable)¶
- Public API surface (count): 0 new public modules. Phase 1 adds private modules only (probes and their per-probe sub-schemas). The public API of
codegenieremains the CLI plus__version__. - New top-level packages: 0. Probes land in the existing
src/codegenie/probes/package; sub-schemas land insrc/codegenie/schema/probes/; catalogs land in a new siblingsrc/codegenie/catalogs/(this is the only new directory — declared explicitly so the synthesizer notices, and because catalogs are data, not code). - Net new Python files in
src/: 5 probe modules + 1 catalog loader + 5 sub-schema JSON files + 1catalogs/native_modules.yaml+ 1catalogs/ci_providers.yaml. ≤ 12 files total. - Net new lines of
src/Python (excluding catalogs): target ≤ 1100 lines, hard ceiling 1500. Each probe should be ~150–250 lines of clear parsing logic plus a 30–50-line__init__-style declaration. - Net new lines of test code: target ≥ 1.4× source-code lines (the test-pyramid bias). Phase 1 lands roughly 1500–1800 lines of test code against ~1100 lines of source — this ratio is the convention, not a vanity metric; see "Test plan" below.
- Test coverage target: ratchet from Phase 0's 85 / 75 floor to 90% line / 80% branch on
src/codegenie/excludingcli.py. (Per final-design §3.3 the floor was always intended to ratchet up at Phase 1; this delivers on that commitment.) - Cyclomatic complexity ceiling per function: 10 (enforced via
ruffruleC901). Probes that exceed it must split into helper functions; this is a Rule 2 (Simplicity First) tripwire. The lockfile parser will likely sit at 8–9 — close, but inside the bound. - Plain Python vs framework-coupled code ratio: ≥ 95% plain Python. The only framework-coupled paths are the
clickdecorator oncli.py(Phase 0) andpydanticinside_ProbeOutputValidator(Phase 0); Phase 1 adds zero new framework couplings. No probe importspydantic,click,structlogdirectly — they use the existing facades. - External tool surface added to
exec.ALLOWED_BINARIES: 1 (node, used byNodeBuildSystemfornode --versionengine-constraint cross-check). Each binary addition requires its own ADR amendment per Phase 0 §2.5. (We deliberately do not addpnpm,npm,yarnas binaries — we read their lockfiles, we do not invoke them. Lockfile-parsing is what makes the gather deterministic and cache-friendly.) - Cache hit rate target on second run (Phase 1 exit-criterion): 100% of Phase 1 probes return
CacheHitwhen nodeclared_inputshave changed. Verified bytests/integration/test_cache_hit_on_real_repo.pyagainst an open-source Node.js fixture. - Wall-clock target for the cold integration run on the real-OSS fixture: p50 ≤ 4s, p95 ≤ 8s. Warm (cache-hit): p50 ≤ 0.4s. These are targets surfaced as advisory CI dashboard metrics, not blocking gates (per Phase 0 §2.11 the cold-start canary is advisory).
- Pre-commit / lint / type-check posture: unchanged from Phase 0.
ruff+mypy --strictonsrc/+forbidden-patternscontinue. Phase 1 must pass the existing hooks with zero suppressions, zero# type: ignore, zero# noqa. If a rule fires legitimately, surface it (Rule 12) — don't suppress it.
Architecture¶
codegenie gather <path>
│
▼
┌────────────────────────────┐
│ Phase 0 CLI entry (click) │ ← unchanged
│ - path validation │
│ - tool-readiness check │
│ - config load │
│ - .gitignore prompt │
└──────────────┬─────────────┘
│
▼
┌────────────────────────────┐
│ Phase 0 Coordinator │ ← unchanged
│ - asyncio.Semaphore │
│ - per-probe Task │
│ - cache lookup / pass-thru│
│ - _ProbeOutputValidator │
│ - OutputSanitizer.scrub │
└──────────────┬─────────────┘
│
▼
┌────────────────────────────────┴────────────────────────────────┐
│ Phase 0 Probe Registry (explicit import — no entry pts) │
│ │
│ language_detection (Phase 0) ← unchanged │
│ │
│ ┌──────────────── Phase 1 additions ──────────────────────────┐ │
│ │ │ │
│ │ node_build_system ┐ │ │
│ │ node_manifest ├─ each: one file in probes/ │ │
│ │ ci │ each: one sub-schema in schema/probes/│ │
│ │ deployment │ each: declares applies_to_languages, │ │
│ │ test_inventory ┘ applies_to_tasks, declared_inputs │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Phase 0 Cache│ │ Phase 0 Audit│ │ Phase 0 Sanitizer│
│ unchanged │ │ unchanged │ │ unchanged │
└──────┬───────┘ └──────────────┘ └──────┬───────────┘
│ │
▼ ▼
.codegenie/cache/ .codegenie/context/
(Phase 0 layout) ├── repo-context.yaml
├── schema-version.txt
├── raw/
│ ├── language_detection.json
│ ├── node_build_system.json
│ ├── node_manifest.json
│ ├── ci.json
│ ├── deployment.json
│ └── test_inventory.json
└── runs/<ts>-<short>.json
Probe Catalogs (data, not code)
──────────────────────────────────
src/codegenie/catalogs/
native_modules.yaml ← NodeManifest reads
ci_providers.yaml ← CI reads
Three things to notice in the diagram:
- Every existing box says "unchanged." This is the test of extension-by-addition. If Phase 1 needed even one box edited, the contract was wrong; per Phase 0 §12, the only legitimate Phase 1 modifications are
exec.ALLOWED_BINARIES(with an ADR amendment) and the addition of new sub-schemas and new probes. - Catalogs are a new sibling directory. They are data, loaded by the probes that consume them. Adding a native module to the catalog must not require touching
node_manifest.py. This isproduction/design.md §2.6("Organizational uniqueness as data, not prompts") applied to internal organizational knowledge. - Each probe owns exactly one slice of
repo-context.yaml.NodeBuildSystemownsbuild_system;NodeManifestownsmanifests;CIownsci;Deploymentownsdeployment;TestInventoryownstest_inventory. No probe writes outside its slice. This is the spine oflocalv2.md §4.
Components¶
LanguageDetectionProbe (extension, not new)¶
- Purpose: Extend Phase 0's
LanguageDetectionProbeto populate the Node-specific fieldslocalv2.md §5.1(A1) requires: framework hints, monorepo markers. The Phase 0 implementation produced only extension counts. - Public interface: unchanged.
run(self, repo: RepoSnapshot, ctx: ProbeContext) -> ProbeOutputper the §4 ABC. - Internal design: the directory walker from Phase 0 stays; what changes is post-walk classification.
- Framework detection: read
package.jsononce (viaNodeManifestProbe's parser, which it does not directly call — see Tradeoffs below) and inspectdependencies+devDependenciesfor known framework markers (nestjs/*,express,fastify,next,koa,hapi). This is a flat dictionary lookup against a constant set; ~20 lines of code. - Monorepo detection: filesystem checks for
pnpm-workspace.yaml,lerna.json,nx.json,turbo.json, andpackage.json#workspacesfield presence. FivePath.exists()calls + one JSON read = ~30 lines. - Dependencies: stdlib only (
json,pathlib). No new deps. - Where it lives:
src/codegenie/probes/language_detection.py(Phase 0 file, extended in place — this is not an exception to extension-by-addition;localv2.md §5.1 A1defines the full A1 schema slice up front, and Phase 0 deliberately shipped a subset with the rest deferred to Phase 1 per phase-0 final-design §2.10). - Tradeoffs accepted:
LanguageDetectionreadspackage.jsonitself rather than depending onNodeManifestProbehaving already run. This violates DRY by a small margin (two probes parsepackage.json), but it preserves probe isolation (localv2.md §2: "Adding a new probe never requires modifying existing ones"). CouplingLanguageDetectiontoNodeManifest's output is the worse failure mode — it would makeLanguageDetection's test fixtures depend on the entireNodeManifesttest surface.- Framework detection is intentionally shallow.
NestJSdetected by@nestjs/corein deps, not by AST-level decorator analysis. The shallow signal is correct forLanguageDetection's role (a quick repo map); deeper detection lives in Phase 2'sNodeReflectionProbe.
NodeBuildSystemProbe¶
- Purpose: Populate the
build_systemslice fromlocalv2.md §5.1 A2. Determines package manager, engine constraints, npm scripts, bundler, TypeScript compilation setup. - Public interface: standard probe ABC.
name = "node_build_system",layer = "A",applies_to_languages = ["javascript", "typescript"],applies_to_tasks = ["*"],requires = ["language_detection"],declared_inputs = ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb", ".nvmrc", ".node-version", "tsconfig.json", ".tool-versions"]. - Internal design:
- Package-manager resolution by lockfile precedence (
bun.lockb>pnpm-lock.yaml>yarn.lock>package-lock.json). Pure stdlib; ~15 lines. package.json#scriptsextraction viajson.loads; values pass through as opaque strings (we record what's there, we do not interpret).- Node version: read
package.json#engines.node,.nvmrc,.node-version,.tool-versions(asdf format) in declared precedence order. Each is a file read with simple string handling. tsconfig.json: read viajson.loads(it's JSON-with-comments in practice; we usetomllib-style permissive parsing — no, we usejson5is a temptation we resist. Instead: read the file, strip line comments (// ...regex) and block comments (/* ... */regex) with a small ~10-line helper, thenjson.loads). This is the only place a non-trivial parser helper lives; if it grows beyond a screen, it moves tosrc/codegenie/parsers/jsonc.py.- Bundler detection: pure data lookup against deps (
webpack,rollup,esbuild,vite,parcel,turbopack) + config-file presence (webpack.config.js,rollup.config.js,vite.config.ts, etc.). node --versioncross-check (optional, gated by tool-readiness): ifnodeis inALLOWED_BINARIESand on$PATH, callnode --versionviaexec.run_allowlistedand record the local version alongside the constraint. Absence is fine; the probe reportsnode_version_pinned: null, node_version_resolved_locally: null, confidence: high(the constraint is the load-bearing fact).- Dependencies: stdlib (
json,pathlib,re). PyYAML (existing Phase 0 dep) for the lockfile YAML cases. - Where it lives:
src/codegenie/probes/node_build_system.py. - Tradeoffs accepted:
- We choose the package manager by lockfile, not by
packageManagerfield inpackage.json. The lockfile is the empirical ground truth; the field is aspirational. Where they disagree, we emitwarnings: ["packageManager field declares 'yarn@4.0.0' but yarn.lock is absent and pnpm-lock.yaml is present"]and prefer the lockfile. - We do not evaluate any script.
commands.build = "pnpm run build"is what the probe records; what that script actually does is the Planner's problem, not ours. - Multiple lockfiles trigger
confidence: low(per "Honest confidence" — final-design andproduction/design.md §2.3). We pick the one with highest precedence and record the conflict; we do not deduplicate.
NodeManifestProbe¶
- Purpose: Populate
manifestsfromlocalv2.md §5.1 A3. The single most distroless-relevant Layer A probe — native module enumeration is the largest source of distroless migration failures. - Public interface: standard probe ABC.
name = "node_manifest",applies_to_languages = ["javascript", "typescript"],applies_to_tasks = ["*"],requires = ["language_detection"],declared_inputs = ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "src/codegenie/catalogs/native_modules.yaml"]. - Internal design:
- Three lockfile parsers, each in its own helper function:
_parse_pnpm_lock,_parse_package_lock,_parse_yarn_lock. The pnpm parser usesyaml.safe_load; the package-lock parser usesjson.loads; the yarn parser is the only non-trivial one (Berry's YAML-ish format) and uses thepyarnlibrary on PyPI if it remains widely supported as of Phase 1 land, falling back to a small hand-rolled parser otherwise. This is the only library decision in Phase 1 that requires evidence at land-time — see "Open questions" below. - Native module catalog:
src/codegenie/catalogs/native_modules.yamlis a flat list of{name, requires_node_gyp, system_deps_required, binary_artifacts_glob, notes}. The probe iterates the lockfile's resolved packages and emits one entry per match. Adding a module is a YAML PR. - Engine declarations: read from
package.json#engines. - Optional / bundled dependencies: counted from
package.json#optionalDependenciesandpackage.json#bundledDependencies. - Dependencies: stdlib + PyYAML + (optionally)
pyarn. Thepyarndependency, if adopted, must pass the Phase 0fencetest (it's a YAML parser, not an LLM SDK — should be fine) and is pinned inpyproject.tomlunder thegatherextra closure. - Where it lives:
src/codegenie/probes/node_manifest.py. Lockfile parsers live insrc/codegenie/probes/_lockfiles/(private helpers). - Tradeoffs accepted:
- The catalog is hand-curated, not derived from npm metadata. This is deliberate: native-module knowledge is organizational + community knowledge that drifts slowly and demands human review when entries are added. Auto-deriving from registry metadata is a Phase 2+ concern, and even then, the catalog stays canonical with auto-derivation as input.
- We parse the lockfile, not
node_modules/. The lockfile is committable, deterministic, cache-friendly;node_modules/is a build artifact and may not be present. This is the only choice consistent with the cache contract. - We do not invoke
npm lsorpnpm list. Those requirenode_modules/and are slow. Lockfile parsing is the deterministic equivalent.
CIProbe¶
- Purpose: Populate
cifromlocalv2.md §5.1 A4. Records which CI provider is in use, which workflows build container images, which test/lint commands run. - Public interface: standard probe ABC.
name = "ci",applies_to_languages = ["*"],applies_to_tasks = ["*"],requires = [],declared_inputs = [".github/workflows/*.yml", ".github/workflows/*.yaml", ".gitlab-ci.yml", ".circleci/config.yml", "Jenkinsfile", "azure-pipelines.yml", "src/codegenie/catalogs/ci_providers.yaml"]. - Internal design:
- Provider detection: read the
ci_providers.yamlcatalog; each entry declares a list of marker paths and a parser function name. First catalog entry whose marker paths exist wins. - GitHub Actions parser:
yaml.safe_loadper workflow file. Extracts job names, the steps in each job, looks fordocker build/docker buildx/docker/build-push-actionto setbuilds_image: true, extracts test/lint scripts fromrun:steps via simple string matching against known patterns (npm run test,pnpm test,yarn test, etc.). - GitLab CI parser: same
yaml.safe_load; the schema is well-documented and stable. - Jenkinsfile: best-effort regex-only parser. It's Groovy; we don't try to be a Groovy parser. Records that Jenkinsfile is present with
confidence: lowandwarnings: ["Jenkinsfile detected; only marker-level recognition implemented"]. This is Rule 12 (fail loud) applied to a probe: the artifact says explicitly that the analysis is partial. - CircleCI / Azure Pipelines: stub recognizers (mark presence with
confidence: low); fuller parsers land when actually needed. - Dependencies: stdlib + PyYAML.
- Where it lives:
src/codegenie/probes/ci.py. Per-provider parsers live insrc/codegenie/probes/_ci_parsers/(private helpers, one file each). - Tradeoffs accepted:
- Multiple providers can coexist in a repo (e.g., GitHub Actions + a legacy Jenkinsfile). The probe records both; the
providerfield becomes a list, not a singleton (this is a small departure fromlocalv2.md §5.1 A4's example output, which shows a singleton — we report this as a doc-update candidate, not a deviation; see Open Questions). Confidence drops if more than one provider is detected. - We do not execute any CI logic or simulate any CI run. We read configuration as data.
DeploymentProbe¶
- Purpose: Populate
deploymentfromlocalv2.md §5.1 A5. Records deployment type (Helm, Kustomize, raw manifests, Terraform), image reference path, health probe paths, security context, exposed ports, required env vars. - Public interface: standard probe ABC.
name = "deployment",applies_to_languages = ["*"],applies_to_tasks = ["*"],requires = [],declared_inputs = ["deploy/**/*.yaml", "deploy/**/*.yml", "k8s/**/*.yaml", "helm/**/*", "chart/**/*", "kustomization.yaml", "kustomization.yml", "*.tf", "main.tf"]. - Internal design:
- Deployment type detection by directory and file marker:
Chart.yaml→ Helm;kustomization.yaml→ Kustomize; rawkind: DeploymentYAML → raw manifests;.tffiles → Terraform. - Helm: read
Chart.yaml+values.yaml; do not render templates (rendering requires a Helm binary and would be non-deterministic for templates usingnowor random functions). Record the image reference path (e.g.,image.repository) and the value at that path invalues.yaml. The Planner can render later if it needs the rendered form. - Kustomize: read
kustomization.yamlresources list; recurse one level. Do not invokekustomize build. - Raw manifests: walk YAML files,
yaml.safe_load_all(multi-document), filter tokind: Deployment | StatefulSet | DaemonSet | Pod, extractspec.template.spec.containers[].image,securityContext,ports,env. - Terraform: optional. If
python-hcl2is available in the gather extras, parse; otherwise emitconfidence: low, warnings: ["Terraform files detected but python-hcl2 not installed; parsing skipped"]. Per Phase 0's stance on optional tooling, we do not addpython-hcl2to required deps in Phase 1 — Terraform-heavy migrations are a Phase 7+ concern. - Dependencies: stdlib + PyYAML.
python-hcl2is optional (declared inpyproject.tomlunder a newgather-terraformextra; not in default install). - Where it lives:
src/codegenie/probes/deployment.py. Per-type parsers insrc/codegenie/probes/_deployment_parsers/. - Tradeoffs accepted:
- We do not render Helm or Kustomize. That's a deliberate determinism choice (
production/design.md §2.4); the rendered form is non-deterministic in general. The Planner is free to render in Phase 3 with full Helm/Kustomize tooling; the gather captures source-level evidence. - Multi-environment deployments (separate
values-prod.yaml,values-staging.yaml) are reported as a list of environments, each with its own image reference.production/design.mddoes not require us to pick "the prod one"; that's a Planner judgment.
TestInventoryProbe¶
- Purpose: Populate
test_inventoryfromlocalv2.md §5.1 A6. Records the test framework, test count, integration/smoke/e2e command paths, coverage data presence. - Public interface: standard probe ABC.
name = "test_inventory",applies_to_languages = ["javascript", "typescript"],applies_to_tasks = ["*"],requires = ["language_detection", "node_build_system"],declared_inputs = ["package.json", "vitest.config.*", "jest.config.*", ".mocharc.*", "playwright.config.*", "test/**/*.test.*", "tests/**/*.test.*", "src/**/*.test.*", "**/*.spec.*", "coverage/lcov.info"]. - Internal design:
- Framework detection: data lookup against
dependencies + devDependenciesforvitest,jest,mocha,tap,node:test(presence ofnode:testis implied byengines.node >= 18),playwright,cypress,@playwright/test. - Test count: a single pass
os.walk(per Phase 0's exclusion conventions —node_modules,dist,build,coverage,.next,.turbo) counting files matching*.test.{js,ts,jsx,tsx,mjs,cjs}and*.spec.{js,ts,jsx,tsx,mjs,cjs}. - Command extraction: read
package.json#scriptsfor entries namedtest,test:unit,test:integration,test:smoke,test:e2e,test:coverage. Record verbatim. - Smoke script presence: filesystem check for
scripts/smoke.sh,scripts/smoke.js,scripts/smoke.ts,tests/smoke/*. - Coverage data: filesystem check for
coverage/lcov.info; if present, parse the totals (smalllcovparser; ~30 lines or use thelcov-parserPyPI library if it remains maintained — decided at land-time via the same evidence-bar aspyarn). - Dependencies: stdlib only (PyYAML already loaded).
- Where it lives:
src/codegenie/probes/test_inventory.py. - Tradeoffs accepted:
- We count test files, not test cases. Counting cases requires running the framework or parsing per-framework
describe/itblocks — both are expensive and noisy. File counts are sufficient for the planner's "is there test coverage?" judgment. node:testis reported asframework: node_test(a value not present in the §5.1 A6 example output). This is a fact, not a judgment; the schema'sframeworkfield accepts string enumeration.
Probe registry — explicit imports¶
- Purpose: Make every probe in the system visible at one place. Per Phase 0 §2.4 (
design-best-practicesand final-design): noentry_pointsscan, no plugin discovery, no auto-import. - Public interface:
src/codegenie/probes/__init__.pycontinues to be the single place where new probes are imported (to trigger their@register_probedecoration). Phase 1 adds fivefrom . import ...lines. - Where it lives:
src/codegenie/probes/__init__.py. - Tradeoffs accepted: explicit imports cost one line of code per probe and grep-ability for "what's in this system." This is the trade we make every time; it's worth it.
Probe sub-schemas¶
- Purpose: Each probe owns one sub-schema declaring the JSON shape of its slice. The Phase 0 envelope (
src/codegenie/schema/repo_context.schema.json)$refs each sub-schema by path. - Where it lives:
src/codegenie/schema/probes/language_detection.schema.json(Phase 0, extended in place for the new fields)src/codegenie/schema/probes/node_build_system.schema.jsonsrc/codegenie/schema/probes/node_manifest.schema.jsonsrc/codegenie/schema/probes/ci.schema.jsonsrc/codegenie/schema/probes/deployment.schema.jsonsrc/codegenie/schema/probes/test_inventory.schema.json- Tradeoffs accepted:
- Each sub-schema is
additionalProperties: falseat its own root (per Phase 0's layered policy: strict at the boundaries where it matters). Adding a field to a probe's output requires editing both the probe code and its sub-schema in the same PR — this is friction, and the friction is the point. - Sub-schemas are JSON files, not Pydantic models. JSON Schema is the contract Phase 0 chose; we honor it (Rule 11). Pydantic stays at the trust-boundary inside the coordinator.
Catalog loader¶
- Purpose: Read YAML data files (native modules, CI providers) once at module import, expose them as immutable mappings.
- Public interface:
src/codegenie/catalogs/__init__.pyexportsNATIVE_MODULES: Mapping[str, NativeModuleEntry]andCI_PROVIDERS: Mapping[str, CIProviderEntry]where the entry types areNamedTuples (stdlib, immutable, type-friendly, deserialization-trivial). - Internal design: read YAML via
yaml.safe_loadat import; build the named-tuple-keyed dict; freeze viatypes.MappingProxyType. Schema-validate the catalog itself againstsrc/codegenie/catalogs/_schema.json(loaded once, validated once at import). Fail-loud if the catalog YAML is malformed (Rule 12). - Where it lives:
src/codegenie/catalogs/{__init__.py, native_modules.yaml, ci_providers.yaml, _schema.json}. - Tradeoffs accepted: loading at import time is a small startup cost (~5ms total) but it's the predictable, conventional way; the alternative (lazy lookup) would scatter "is the catalog loaded yet?" checks throughout the probe code.
Data flow¶
A representative Phase 1 run on a real Node.js repo (acme/billing-service, ~1k files, TypeScript + NestJS + pnpm + GitHub Actions + Helm):
- CLI entry (Phase 0). Path validated; tool-readiness check now includes
node(optional) in addition togit. Config loaded. RepoSnapshotconstruction (Phase 0).git rev-parse HEADviaexec.run_allowlisted. The snapshot now carriesdetected_languages: {"typescript": 247, "javascript": 32, ...}afterLanguageDetectionProberuns.- Probe registry filter (Phase 0).
for_task("__bullet_tracer__", {"typescript"})now returns[LanguageDetection, NodeBuildSystem, NodeManifest, CI, Deployment, TestInventory].LanguageDetectionhas no dependencies; the other four depend on it via therequiresfield. The coordinator's topological order:LanguageDetectionfirst (alone, completes in ~80ms), then the remaining five in parallel. - Coordinator dispatch (Phase 0).
Semaphore(min(cpu_count(), 8)); oneasyncio.Taskper probe;asyncio.wait_forwith each probe'stimeout_seconds(default 30s for these probes — they're all file-parsing, no subprocess except the optionalnode --version). - Per-probe cache lookup (Phase 0). For each probe, compute
cache_key = identity_hash(probe_name, probe_version, schema_version, content_hash(declared_inputs))per phase-0 final-design §2.7. Cold first run: all misses. - Probe execution (Phase 1, new). Each probe parses its files, builds its slice, returns a
ProbeOutput. Per-probe wall-clock on the 1k-file fixture (target, p50):LanguageDetection80ms,NodeBuildSystem40ms,NodeManifest250ms (lockfile parse dominates),CI50ms,Deployment60ms (Helm chart walk),TestInventory120ms (file count walk). Parallel wall-clock dominated byNodeManifestat ~250ms. _ProbeOutputValidator(Phase 0). EachProbeOutputvalidated againstJSONValuerecursive type + field-name regex. Phase 1 probes use field names likepackage_manager,native_modules,image_reference,framework— none trip the secret-name regex. (If a future probe needs a field literally namedauth_token, this is a design issue to surface, not suppress.)OutputSanitizer.scrub(Phase 0). Absolute paths in lockfile entries (e.g.,binary_artifacts: ["/Users/me/work/billing/node_modules/sharp/build/..."]) get scrubbed to relative paths. This is load-bearing for Phase 11 (final-design §2.8) — the.codegenie/artifacts will eventually be committed to repos, and developer home paths must not leak.- Cache write (Phase 0). Each
ProbeOutputblob written via the BLAKE3-keyed, two-shard-char layout; index appended. - Output merge (Phase 0). Each probe's
schema_slicemerged into the top-levelrepo-context.yamlenvelope. - Schema validation (Phase 0).
Draft202012Validatorruns against the full envelope; the layeredadditionalPropertiespolicy means: strict at the envelope (extra root keys rejected), strict per-probe-sub-schema, loose between (an unknown probe's slice isprobes.unknown_probe: <object>— accepted by the envelope'sadditionalProperties: trueonprobes.*). - Raw artifact writing (Phase 0). Each probe's raw output (the full parsed lockfile, the full parsed Helm chart, etc.) is written to
.codegenie/context/raw/<probe>.json, 0600. - YAML write (Phase 0).
repo-context.yaml.tmp→os.replace.CSafeDumper. - Audit record (Phase 0). Per-probe
(name, version, cache_hit, wall_clock, exit_status, warnings_count)recorded; final YAML SHA-256 included. - Exit 0.
Second run (cache hit path). Same invocation, no source files changed:
1. Steps 1–5 identical until per-probe cache lookup.
2. Each probe's cache lookup hits (same declared_inputs, same content). Coordinator records ProbeExecution.CacheHit for each, returns the cached ProbeOutput directly (per phase-0 final-design §2.6, cache-hit pass-through preserved as a first-class coordinator output).
3. No probe run() is called; no parsing happens; no subprocess.
4. Steps 8 (sanitizer), 11 (schema validation), 12–14 still run because the merge must happen (cache stores the per-probe slice, the merge into a single YAML happens fresh each run — this keeps the YAML's gathered_at timestamp honest).
5. Wall-clock target on the 1k-file fixture for the cache-hit run: p50 ≤ 0.4s.
The data flow honors three of the load-bearing commitments explicitly:
- Probe contract (localv2.md §4) holds across all six probes; the snapshot test in test_probe_contract.py (Phase 0) is unmodified and continues to pass.
- Cache-hit pass-through is exercised in steps 2–4 of the second run; this is the exit-criterion "cache hits on second run."
- Idempotent activities — each probe is referentially transparent on its declared_inputs. Phase 9's Temporal Activities will wrap each of these probes directly; no rewrite needed.
Failure modes & recovery¶
| Failure | Detected by | Recovery |
|---|---|---|
NodeBuildSystem finds multiple lockfiles |
Probe internal logic | confidence: low; warnings: [list of lockfiles]; pick by precedence; gather continues |
NodeManifest lockfile parse error (malformed pnpm-lock.yaml) |
LockfileParseError raised in helper |
Caught by probe; ProbeOutput(errors=["lockfile parse failed: ..."], confidence: low, schema_slice={"manifests": [...partial...]}); gather continues |
| Native module catalog missing or malformed | CatalogLoadError at module import |
Hard fail at CLI startup with a clear "the native_modules.yaml catalog is malformed" message and the path to the file. This is Rule 12 (fail loud) — the catalog is load-bearing data, silent fallback is wrong. |
CI probe finds Jenkinsfile only |
Probe internal logic | confidence: low; warnings: ["Jenkinsfile detected; only marker-level recognition implemented"]; gather continues, slice has provider + path but no extracted commands |
Deployment finds no recognized deployment |
Probe internal logic | Schema slice {"type": null, "confidence": "low", "warnings": ["no recognized deployment manifests found"]}; gather continues |
Deployment finds Terraform but python-hcl2 absent |
Optional-dep ImportError caught | confidence: low; warnings: ["python-hcl2 not installed; Terraform parsing skipped"]; gather continues |
TestInventory finds no test files at all |
Probe internal logic | test_count: 0; confidence: medium (the absence is itself a fact, but it's worth flagging); warnings: ["no test files matching standard patterns found"]; gather continues |
tsconfig.json is JSONC with unsupported syntax |
JSONDecodeError after comment-strip |
confidence: low; warnings: ["tsconfig.json contains non-standard syntax; partial parse"]; gather continues with whatever fields parsed |
Probe exceeds its timeout_seconds |
Phase 0 coordinator (asyncio.wait_for) |
Phase 0 handling unchanged: ProbeOutput(errors=["timeout"], confidence: low); gather continues |
node --version fails (binary absent, exec error) |
exec.run_allowlisted raises |
Probe catches; node_version_resolved_locally: null; does not affect probe confidence — the constraint from package.json#engines is the load-bearing fact |
Path traversal in declared_inputs (e.g., a probe author writes "../etc/passwd") |
Phase 0 registration test (test_path_traversal.py) |
Hard fail at registration time; the test never lets the probe register |
| Schema sub-schema malformed | jsonschema schema-compile error at module load |
Hard fail at CLI startup with a clear message pointing to the malformed sub-schema file |
Sub-schema $ref resolves to nonexistent path |
jsonschema reference resolution error |
Hard fail at CLI startup; envelope tests assert all $refs resolve |
| Probe slice fails its own sub-schema | Draft202012Validator at envelope level |
YAML written with .invalid suffix; CLI exits 3 (Phase 0 convention) |
| Two probes attempt to write the same top-level slice key | Coordinator merge logic | Hard fail with DuplicateSchemaSliceError; test tests/unit/test_probe_slice_disjoint.py asserts statically |
The pattern: deterministic facts about messy reality, explicit confidence, never silent degradation. Every probe that meets its inputs in an unexpected state surfaces a typed warning rather than guessing. Every hard-fail is a load-bearing-invariant violation (Rule 12).
Resource & cost profile¶
- Tokens per run: 0. Phase 1 is deterministic gather end-to-end. The Phase 0
fenceCI job continues to assert this structurally. - Wall-clock per
codegenie gatheron the real-OSS fixture (expressjs/expressshape — ~200 source files, no native modules, GitHub Actions, no Helm), target p50 / p95: - Cold (cache empty): 1.5s / 3s
- Warm (cache full): 0.3s / 0.6s
- Wall-clock per
codegenie gatheron the 1k-file billing-service-shape fixture: - Cold: 4s / 8s (dominated by
NodeManifestlockfile parse and theTestInventorywalk) - Warm: 0.4s / 1s
- Memory: ~80 MB RSS at peak (Phase 0 ~70 MB; +10 MB for the catalogs and the additional probe state).
- Storage growth per gather:
repo-context.yaml~20–40 KB;raw/~200–400 KB (lockfile dumps dominate); cache blobs ~50 KB total; audit ~5 KB. Per-repo per-gather: ~0.5 MB. After a year of nightly continuous gather: ~180 MB per repo (well within local-disk tolerances; Phase 14 will revisit when continuous gather lands at portfolio scale). - CI walltime impact: the Phase 0 90s p95 advisory target should hold; Phase 1 adds five probe unit-test modules (each ~30 tests) plus one integration test against a real OSS fixture cloned at test time (~3s, cached by
actions/cacheon a checksum of the fixture's commit SHA). Estimated CI delta: +25s p50, +45s p95. - Convention cost (where best practices buy future-proofing at present-day expense):
- Per-probe sub-schemas + the
additionalProperties: falsediscipline cost ~30 minutes of authoring time per probe versus a single loose top-level schema. Pays for itself the first time Phase 7 (distroless migration) adds a probe and the strict envelope rejects a typo at land-time rather than at downstream-consumer time. - The catalog separation (
native_modules.yaml,ci_providers.yaml) costs ~150 lines of YAML and one schema file. Pays back the first time someone addsnode-rdkafkato the catalog as a YAML PR rather than a Python PR. - The probe-isolation discipline (each probe re-reads
package.jsonrather than depending on another probe's parsed output) costs ~30 lines of duplicated parser-invocation glue. Pays back permanently — every probe is independently testable, independently cacheable, and independently lift-able to the service per ADR-0007 production.
Test plan¶
The test pyramid here is wider at the unit base than at the integration top. Each probe is unit-tested exhaustively against fixture inputs before the integration test cares whether they compose correctly.
Unit tests (tests/unit/probes/)¶
One test module per probe. Each follows the same shape (predictable, not clever):
| Test module | Asserts |
|---|---|
test_language_detection.py |
(Phase 0 tests retained.) New: framework detection from dependencies (NestJS, Express, Fastify, Next, Koa, Hapi); monorepo markers (pnpm-workspace.yaml, lerna.json, nx.json, turbo.json, package.json#workspaces); confidence reporting (medium when only one weak signal). |
test_node_build_system.py |
Lockfile-precedence selection (each of pnpm, yarn, npm, bun on isolated fixtures); multi-lockfile detection drops confidence; engines.node precedence over .nvmrc over .node-version; tsconfig.json with comments parses; bundler detection per bundler; package.json malformed → confidence: low, errors: [...]. ~25 tests. |
test_node_manifest.py |
Each lockfile parser on a fixture for that format; native-module detection against the catalog (one fixture per cataloged module: bcrypt, sharp, better-sqlite3, node-canvas); optionalDependencies counting; bundledDependencies counting; lockfile integrity bit reported; cache-key stability across runs. ~30 tests. |
test_ci.py |
GitHub Actions parsing (one workflow that builds an image; one that doesn't; one with a matrix); GitLab CI; Jenkinsfile (presence only); multiple-provider repo; absent CI directory. ~20 tests. |
test_deployment.py |
Helm Chart.yaml + values.yaml; Kustomize; raw Deployment manifest; raw Pod (skipped — pods aren't deployments); multi-environment Helm (values-prod.yaml + values-staging.yaml); Terraform with python-hcl2 present (skip if not installed in CI); securityContext extraction; required env vars from envFrom and direct env. ~25 tests. |
test_test_inventory.py |
Vitest, Jest, Mocha, Tap, node:test, Playwright, Cypress detection; test-file count walks honor exclusions; package.json#scripts extraction for the recognized script names; smoke-script presence; coverage lcov.info parsing (totals only); empty test directory case. ~20 tests. |
test_catalogs.py |
Catalog YAML parses; catalog schema validates; every catalog entry has the required fields; duplicate names rejected at load. |
test_probe_registration.py (extends Phase 0) |
Each Phase 1 probe registers exactly once; requires graph is acyclic; LanguageDetection has no requires; the other five all declare requires=["language_detection"] correctly; applies_to_languages is non-empty for Node-only probes; applies_to_tasks=["*"] for all Layer A probes. |
test_probe_slice_disjoint.py (extends Phase 0) |
Asserts statically that no two registered probes write to the same top-level schema_slice key. This catches accidental collisions at test time rather than at merge time. |
test_sub_schemas.py |
Each per-probe sub-schema is itself a valid JSON Schema (Draft 2020-12); each $ref from the envelope resolves; each sub-schema has additionalProperties: false at its root; round-trip a representative slice through the envelope validator. |
test_cache_keys.py (extends Phase 0) |
For each Phase 1 probe, modifying a file in declared_inputs changes the cache key; modifying a file not in declared_inputs does not change the cache key (this is the contract that makes incremental gathers correct). |
Coverage target: 90% line / 80% branch per the ratchet from Phase 0.
Integration tests (tests/integration/)¶
| Test module | Asserts |
|---|---|
test_layer_a_end_to_end.py |
Runs the full codegenie gather flow against the committed fixture at tests/fixtures/node_typescript_helm/ (a minimal but realistic NestJS-on-Helm repo); asserts every Phase 1 probe produces a non-empty slice; asserts the full repo-context.yaml validates against the envelope schema; asserts every cross-probe reference holds. |
test_cache_hit_on_real_repo.py |
Runs gather twice against the same fixture; asserts the second run produces zero ProbeExecution.Ran for the Phase 1 probes (all CacheHit); asserts wall-clock ratio second / first ≤ 0.25 (advisory metric). |
test_cache_invalidation.py |
Runs gather; modifies package.json (adds a dependency); runs gather again; asserts NodeBuildSystem, NodeManifest, LanguageDetection, and TestInventory produce Ran (their declared_inputs changed) and CI, Deployment produce CacheHit (their declared_inputs did not). |
test_real_oss_fixture.py |
Clones expressjs/express (pinned commit SHA) at test setup time; runs gather; asserts schema validity, asserts no probe crashed, asserts the manifest probe detects no native modules (Express has none), asserts the CI probe detects GitHub Actions. Cached by actions/cache on the commit SHA. |
Golden files (tests/golden/)¶
For the integration test against the committed fixture, the expected repo-context.yaml is committed at tests/golden/node_typescript_helm/repo-context.yaml. The test diffs the live output against it. Updating the golden file is an explicit make update-goldens step that requires the developer to inspect the diff. This is the pattern Phase 2 will scale up (per roadmap); Phase 1 lands the convention with one fixture.
Property tests¶
Deferred to Phase 2. Phase 1's probes have no obvious universal invariants the way the cache and sanitizer do (the property-test backbone is for all inputs, this invariant holds; probes are file-shape-specific). Phase 2's IndexHealthProbe will introduce real invariants (e.g., "coverage_pct = files_indexed / files_in_repo") that property-test cleanly.
What is explicitly not in Phase 1 tests¶
- No tests against running CI providers (no live
gh actionscalls; we read configuration files only). - No tests requiring Docker or
node_modulesto be installed. - No tests of probes Phase 1 didn't ship.
- No tests of
IndexHealthProbe(Phase 2). - No tests of cache TTL expiration (Phase 0 covers it; we don't re-test).
- No fuzz tests on lockfile parsers (Phase 8 risk-budget; the lockfile formats are well-understood and the parsers are small).
Risks (top 3–5)¶
- The native-module catalog is the system's blast radius for distroless migration accuracy, and Phase 1 owns it. A missed native module → a Phase 7 distroless migration that builds and passes tests but crashes at runtime because
libvipsis missing. Mitigation: the catalog ships in Phase 1 with the well-known set (bcrypt,sharp,better-sqlite3,node-canvas,node-rdkafka,node-pty,bufferutil,utf-8-validate,argon2,keytar); ADR-amendment workflow exists for additions; Phase 7's integration tests will exercise the catalog and surface gaps. Phase 1's job is to land a correct seed, not a complete enumeration. - Multi-environment Helm and Kustomize deployments expose ambiguity the schema barely captures. The schema in
localv2.md §5.1 A5shows a singleimage_reference; reality has prod / staging / dev overlays. We resolve by emitting a list; this technically deviates from §5.1's example output. The risk: a downstream consumer expects a singleton and breaks. Mitigation: the schema (the JSON Schema, not the doc's example) definesimage_referenceas either an object or a list of objects; the envelope's strict validation surfaces the mismatch loudly if a consumer's expectations drift. We file a doc-update PR againstlocalv2.mdper the ADR-0007 workflow. - The Jenkinsfile parser is intentionally shallow (regex-level marker recognition only). A repo whose entire CI lives in Jenkinsfile will produce a
confidence: lowcislice that the Planner correctly downgrades to "uncertain" — but the slice still has to exist (the envelope requires thecikey). Risk: a downstream consumer interprets "presence ofcikey +confidence: low" as "CI is configured and the gather is ambiguous" rather than "we don't actually know what the CI does." Mitigation: thewarningsarray is explicit, and the schema forcirequires aprovider_recognized: boolfield — whenfalse, the Planner has a structural signal, not just a probabilistic one. - The
pyarnlibrary is the only Phase 1 non-stdlib parser choice not already in Phase 0's dependency closure, and its maintenance status is the bet. Mitigation: at land-time, the implementer confirms (a) the library is still maintained (last release < 18 months), (b) the latest version passes our test fixtures, and (c) a fallback hand-rolled yarn-lock parser is in place if it's not. The fallback is ~100 lines of code; the cost is ours to pay if we have to. - Coverage ratchet from 85/75 to 90/80 may pinch on the deployment probe, which has many narrow branches (per-deployment-type) and limited diminishing-returns past 85%. Mitigation: if 90% line proves unreachable without gameable tests (Rule 9), we lower the per-module floor for
deployment.pyto 85% and surface it explicitly in the PR (Rule 12); we do not ratchet the project-wide target down. Per-module floor exemptions live inpyproject.tomland require an ADR amendment.
Acknowledged blind spots¶
- I have not designed for what happens when
LanguageDetectiondecides "this isn't a Node.js repo." All five Phase 1 probes declareapplies_to_languages = ["javascript", "typescript"]. IfLanguageDetectionreports a Go-only or Python-only repo, the coordinator'sfor_taskfilter skips them all and the YAML envelope has emptybuild_system,manifests, etc. slices — but the schema requires these keys. The envelope schema needs nullable variants for the Layer A Node slices, which deviates fromlocalv2.md §7's example (which assumes Node throughout). The synthesizer should pick a stance: nullable slices, conditional schema branches, or a "Layer A is Node-specific; non-Node repos use a different envelope" position. My recommendation is conditional schema branches keyed onlanguage_stack.primary, but it's not free — it adds schema complexity that may not pay back until Phase 7+. - I have not designed for the case where
package.jsonitself is a manifest fragment from a workspace root (i.e.,package.json#workspacesexists, butpackage.json#dependenciesis empty because all dependencies are inpackages/*/package.json).NodeManifestwill reportdirect_dependencies.production: 0andconfidence: high, which is structurally true but misleading. A futureBuildGraphProbe(Phase 2's B5) resolves the monorepo case fully; Phase 1'sNodeManifestis correctly scoped to "manifests as facts," but the consumer experience on a workspace root may be confusing. The fix is a doc fix (CONTEXT_REPORT.mdtemplate gains a "workspace root detected" note in Phase 1.5 or Phase 2). - The performance lens will argue for parallel lockfile parsing within
NodeManifestfor monorepos with manypackage.jsonfiles. Phase 1 ships sequential parsing. For monorepos with 100+ packages, this might cost ~2–4s. My position: best-practices says ship the readable serial version, measure on a real monorepo fixture in Phase 2, optimize if the measurement demands it. The synthesizer should weigh this against the performance lens's likely "parallelize at the lockfile level" argument. - I have not designed CI behavior on missing optional binaries (
node --versionforNodeBuildSystem). The Phase 0 tool-readiness cache treatsnodeas advisory; CI runners may have it, may not. The probe handles absence gracefully (returnsconfidence: highwithnode_version_resolved_locally: null), but this means the CI matrix doesn't fully exercise theexec.run_allowlisted("node", ...)path unless we explicitly install Node. My recommendation: install Node 20.x in the CI matrix (setup-node@v4action, pinned by SHA per Phase 0 §3.2) so the path is exercised. The synthesizer may want to weigh whether this couples Phase 1's CI to a binary that Phase 2 may not need. - The security lens will likely push for runtime checks against tree-traversal in
declared_inputsglob expansion. Phase 0 tests this at registration time; Phase 1 inherits. If glob expansion at gather-time can be exploited (e.g., a maliciously-crafted symlink in the analyzed repo), the threat is real but it's a Phase 0 layer, not a Phase 1 one. I'm leaving this for the security lens to surface as a structural concern at the right layer.
Open questions for the synthesizer¶
- Nullable Layer A slices for non-Node repos. Should the envelope schema use conditional branches (
if language_stack.primary == "typescript" then ...), nullable fields withconfidence: not_applicable, or a separate envelope per language family? Best-practices favorsconfidence: not_applicable+ explicitapplies_to_languagesfiltering at the registry level — the envelope schema treats Layer A slices as optional rather than nullable, and the JSON Schema marks them as such. This is the minimum-change path; the synthesizer should validate againstlocalv2.md §7's envelope. pyarnadoption decision rule. Adopt at land-time if maintained (< 18 months since last release) and the test fixtures pass; otherwise ship a 100-line hand-rolled parser. Best-practices favors written-down decision rules; the synthesizer should encode the rule (or pick definitively now).packageManagerfield handling.package.json#packageManager(e.g.,"pnpm@8.15.0") is a relatively new field meant to declare the canonical package manager. Where it disagrees with the lockfile, best-practices says prefer the lockfile (it's the empirical truth). The synthesizer may have evidence I don't have on team practice here.- GitHub Actions parser depth. Phase 1 ships the minimum to populate the §5.1 A4 example schema (provider, builds_image, test/lint commands, matrix). The performance lens will likely argue for a deeper parser (every step, every secret reference, every reusable workflow); the security lens will likely argue for at minimum a "secrets referenced" extraction. Best-practices favors landing the minimum and growing it with evidence — the YAML will keep evolving with GitHub's spec; pinning a deep parser now will require revisiting. The synthesizer should pick the depth.
nodebinary inALLOWED_BINARIES. Phase 1 addsnodefor the optionalnode --versioncross-check inNodeBuildSystem. The decision is justified by ADR amendment in Phase 1; the synthesizer should confirm this is the right call versus readingengines.nodeonly. My position: read both; surface the disagreement when present.- Helm rendering vs. parsing. Phase 1 parses source. The synthesizer may argue that without rendering, the
image_referencerecorded is sometimes a templated string ({{ .Values.image.repository }}) and the actual value is invalues.yaml. The probe handles this by emitting both the path invalues.yamland the templated string from the manifest, but a real-world test against five Helm charts at synthesizer review would close the loop better than my fixture coverage. - Catalog-versioning story. The native-module catalog is data, not code; how do we version it? Best-practices says use a
catalog_version: intfield in the YAML and include it in the cache key. The synthesizer should confirm — without versioning, a catalog update silently invalidates cachedNodeManifestoutputs (which may actually be desirable; the synthesizer picks).