agent-doc-system 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/INSTALL_FOR_AGENTS.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/adapters/ansible.mjs +306 -0
  5. package/adapters/cache.mjs +27 -0
  6. package/adapters/contract-consumers.mjs +56 -0
  7. package/adapters/database.mjs +116 -0
  8. package/adapters/deploy-config.mjs +34 -0
  9. package/adapters/dockerfile.mjs +26 -0
  10. package/adapters/events.mjs +145 -0
  11. package/adapters/generic.mjs +86 -0
  12. package/adapters/github-actions.mjs +106 -0
  13. package/adapters/go.mjs +91 -0
  14. package/adapters/graphql.mjs +28 -0
  15. package/adapters/index.mjs +56 -0
  16. package/adapters/node.mjs +118 -0
  17. package/adapters/object-store.mjs +64 -0
  18. package/adapters/oidc.mjs +56 -0
  19. package/adapters/openapi.mjs +50 -0
  20. package/adapters/pnpm.mjs +42 -0
  21. package/adapters/protobuf.mjs +35 -0
  22. package/adapters/registry.mjs +27 -0
  23. package/adapters/resource-client.mjs +61 -0
  24. package/adapters/turborepo.mjs +135 -0
  25. package/adapters/typescript.mjs +85 -0
  26. package/adapters/wrangler.mjs +206 -0
  27. package/bin/agentdoc.mjs +494 -0
  28. package/bin/commands.mjs +169 -0
  29. package/bin/usage.txt +28 -0
  30. package/core/acceptances.mjs +69 -0
  31. package/core/audit.mjs +456 -0
  32. package/core/authority/classes.mjs +39 -0
  33. package/core/authority/engine.mjs +266 -0
  34. package/core/authority/facts.mjs +41 -0
  35. package/core/codes.mjs +126 -0
  36. package/core/compile.mjs +1018 -0
  37. package/core/contracts.mjs +121 -0
  38. package/core/derive.mjs +102 -0
  39. package/core/descriptors.mjs +489 -0
  40. package/core/determinism.mjs +95 -0
  41. package/core/discover.mjs +147 -0
  42. package/core/facts.mjs +53 -0
  43. package/core/fsx.mjs +348 -0
  44. package/core/graph.mjs +260 -0
  45. package/core/impact.mjs +175 -0
  46. package/core/indexes.mjs +165 -0
  47. package/core/jsonschema.mjs +298 -0
  48. package/core/normative.mjs +171 -0
  49. package/core/observations.mjs +129 -0
  50. package/core/provenance.mjs +83 -0
  51. package/core/query.mjs +384 -0
  52. package/core/relations.mjs +286 -0
  53. package/core/scaffold.mjs +322 -0
  54. package/core/secrets.mjs +92 -0
  55. package/core/sourcescan.mjs +88 -0
  56. package/core/toml.mjs +163 -0
  57. package/core/yaml.mjs +477 -0
  58. package/docs/ADAPTERS.md +207 -0
  59. package/docs/AUTHORITY.md +269 -0
  60. package/docs/CI.md +138 -0
  61. package/docs/COMPARISON-KODA.md +164 -0
  62. package/docs/EVALS.md +166 -0
  63. package/docs/MIGRATION.md +261 -0
  64. package/docs/PROVENANCE.md +133 -0
  65. package/docs/QUERY.md +180 -0
  66. package/docs/SPEC.md +339 -0
  67. package/docs/VERIFICATION.md +28 -0
  68. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  69. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  70. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  71. package/docs/assets/readme/why-agentdoc.webp +0 -0
  72. package/docs/ci/generic-ci.sh +28 -0
  73. package/docs/ci/github-actions.yml +35 -0
  74. package/evals/harness.mjs +426 -0
  75. package/evals/scenarios/fixture-a-01-change-api.json +9 -0
  76. package/evals/scenarios/fixture-a-02-change-db.json +9 -0
  77. package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
  78. package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
  79. package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
  80. package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
  81. package/evals/scenarios/fixture-b-04-journey.json +9 -0
  82. package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
  83. package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
  84. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
  85. package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
  86. package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
  87. package/evals/scenarios/koda-02-change-db-schema.json +20 -0
  88. package/evals/scenarios/koda-03-change-auth.json +21 -0
  89. package/evals/scenarios/koda-04-add-event-producer.json +19 -0
  90. package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
  91. package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
  92. package/evals/scenarios/koda-07-change-shared-package.json +16 -0
  93. package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
  94. package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
  95. package/evals/thresholds.json +11 -0
  96. package/package.json +46 -4
  97. package/schemas/api.schema.json +84 -0
  98. package/schemas/common.schema.json +147 -0
  99. package/schemas/component.schema.json +44 -0
  100. package/schemas/config.schema.json +801 -0
  101. package/schemas/domain.schema.json +14 -0
  102. package/schemas/graph.schema.json +1084 -0
  103. package/schemas/journeys.schema.json +47 -0
  104. package/schemas/observation.schema.json +143 -0
  105. package/schemas/resource.schema.json +28 -0
  106. package/schemas/system.schema.json +26 -0
  107. package/skills/agent-doc-system/SKILL.md +117 -0
  108. package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
  109. package/skills/agent-doc-system/references/cli.md +97 -0
  110. package/skills/agent-doc-system/references/create-migrate.md +113 -0
  111. package/skills/agent-doc-system/references/maintenance.md +52 -0
  112. package/templates/ADR.md +26 -0
  113. package/templates/ARCHITECTURE.md +19 -0
  114. package/templates/CONSTRAINTS.md +17 -0
  115. package/templates/PRODUCT.md +17 -0
  116. package/templates/api.yaml +10 -0
  117. package/templates/ci.yml +31 -0
  118. package/templates/component.yaml +14 -0
  119. package/templates/domain.yaml +6 -0
  120. package/templates/journey.yaml +7 -0
  121. package/templates/observation.yaml +19 -0
  122. package/templates/resource.yaml +8 -0
  123. package/templates/system.yaml +10 -0
  124. package/templates/warning-acceptance.yaml +14 -0
@@ -0,0 +1,60 @@
1
+ # Install AgentDoc — instructions for an AI coding agent
2
+
3
+ This document has exactly one job: get the `agentdoc` CLI installed and verified
4
+ on the current machine. It is **not** a project workflow. Do not look for a
5
+ project root, do not run `init`, `audit`, `scaffold`, or `check`, do not create
6
+ `.agentdoc/` or `agentdoc/`, and do not modify any software project. The task
7
+ ends when the CLI is installed and verified.
8
+
9
+ ## 1. Inspect the environment
10
+
11
+ ```bash
12
+ uname -a # OS and architecture
13
+ echo "$SHELL" # current shell
14
+ node --version # is Node present, and which major
15
+ npm --version # is npm present
16
+ command -v git # is git present (needed to install from GitHub)
17
+ ```
18
+
19
+ ## 2. Ensure a supported Node
20
+
21
+ AgentDoc requires **Node 22 or newer** (22, 24 and 26 are the tested
22
+ baselines). If `node` is missing or older than 22, install or switch to a
23
+ supported release using whatever the machine already has — `nvm`, `fnm`,
24
+ `mise`, `brew`, or the package manager — then re-check `node --version`.
25
+
26
+ If no Node toolchain exists and none can be installed without a human (for
27
+ example a managed machine), stop and report that prerequisite.
28
+
29
+ ## 3. Install or upgrade the CLI
30
+
31
+ Canonical install, from the AgentDoc GitHub repository:
32
+
33
+ ```bash
34
+ npm install -g github:lbframe/agent-doc-system
35
+ ```
36
+
37
+ The command is idempotent: run it again to upgrade to the latest `main`.
38
+ If a specific ref is required, append it: `github:lbframe/agent-doc-system#<ref>`.
39
+
40
+ The package has zero runtime dependencies. If `npm install -g` fails on
41
+ permissions, do **not** use `sudo`; fix the npm global prefix instead
42
+ (`npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to `PATH`),
43
+ or report the blocker.
44
+
45
+ ## 4. Verify the binary resolves
46
+
47
+ ```bash
48
+ command -v agentdoc # must print a path
49
+ agentdoc --version # must print a version
50
+ agentdoc doctor # machine self-check; must exit 0
51
+ ```
52
+
53
+ `agentdoc doctor` reports the Node version, the schema bundle and the compiler
54
+ version, and fails if the installation itself is unhealthy. It never requires a
55
+ project — run it from anywhere.
56
+
57
+ ## 5. Report and stop
58
+
59
+ Report the installed version and that `doctor` passed. The installation task is
60
+ complete. Do not proceed into any repository, catalog, or migration work.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lbframe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # AgentDoc
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![AgentDoc — Give coding agents the context they actually need](docs/assets/readme/agentdoc-hero.webp)
4
+
5
+ **Give coding agents the context they actually need.**
6
+
7
+ AgentDoc turns your codebase, docs, contracts, and runtime evidence into a context layer your coding agent can query before it changes anything.
8
+
9
+ [![npm](https://img.shields.io/npm/v/agent-doc-system.svg)](https://www.npmjs.com/package/agent-doc-system) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![node](https://img.shields.io/badge/node-22%20%7C%2024%20%7C%2026-43853d.svg)](https://nodejs.org/)
10
+
11
+ ## Give this to your coding agent
12
+
13
+ Paste this into your agent **from the repository you want to use AgentDoc on**:
14
+
15
+ ```text
16
+ Set up AgentDoc for this repository and use it as the context layer for your work.
17
+
18
+ AgentDoc: https://github.com/lbframe/agent-doc-system
19
+
20
+ 1. Read INSTALL_FOR_AGENTS.md and install or upgrade the AgentDoc CLI.
21
+ 2. Read skills/agent-doc-system/SKILL.md.
22
+ 3. Run agentdoc doctor --project and agentdoc audit.
23
+ 4. Use the appropriate CREATE or MIGRATE workflow for this repository.
24
+ 5. Do not invent ownership, architecture, authority rules, or product intent. Ask me only for decisions AgentDoc cannot safely derive.
25
+ 6. Finish setup with agentdoc validate, agentdoc compile, and agentdoc check.
26
+ 7. From now on, use agentdoc query and agentdoc impact before changes, then agentdoc check after changes.
27
+ 8. Report anything unresolved or conflicting instead of guessing.
28
+ ```
29
+
30
+ That is the intended onboarding path: **give the prompt to the agent, let AgentDoc build the context layer, and keep using it during development.**
31
+
32
+ ## Why AgentDoc?
33
+
34
+ Coding agents can read code. The hard part is knowing **what matters for this change**.
35
+
36
+ Important context is scattered across code, documentation, API contracts, architectural decisions, CI rules, and production reality. Agents either load too much or miss something important.
37
+
38
+ AgentDoc gives them a map.
39
+
40
+ - **Query the right context** for a file or component.
41
+ - **See the blast radius** before changing code.
42
+ - **Detect drift** between code and documentation.
43
+ - **Surface conflicts** instead of silently guessing.
44
+ - **Verify constraints in CI** before stale knowledge becomes trusted context.
45
+
46
+ ![AgentDoc in action — query, impact, check](docs/assets/readme/agentdoc-in-action.webp)
47
+
48
+ ## Three commands, one workflow
49
+
50
+ ```bash
51
+ agentdoc query src/payments/webhook.ts
52
+ agentdoc impact src/payments/webhook.ts
53
+ agentdoc check
54
+ ```
55
+
56
+ `query` tells the agent what it needs to know. `impact` shows what the change can affect. `check` verifies that the repository still agrees with itself.
57
+
58
+ ## Why not just AGENTS.md or RAG?
59
+
60
+ ![Why AgentDoc](docs/assets/readme/why-agentdoc.webp)
61
+
62
+ AgentDoc is not another documentation file and it is not just semantic search.
63
+
64
+ It combines **authored intent**, **facts derived from code**, and **runtime observations** without pretending they are the same thing. When sources disagree, AgentDoc surfaces the conflict instead of choosing a convenient answer.
65
+
66
+ ## From repository knowledge to agent-ready context
67
+
68
+ AgentDoc turns scattered repository knowledge into task-specific context your coding agent can query, verify, and act on.
69
+
70
+ ![How AgentDoc works](docs/assets/readme/how-agentdoc-works.webp)
71
+
72
+ ## Install manually
73
+
74
+ Requires Node.js 22+.
75
+
76
+ ```bash
77
+ npm install -g agent-doc-system
78
+ agentdoc doctor
79
+ ```
80
+
81
+ Then, inside a repository:
82
+
83
+ ```bash
84
+ agentdoc audit
85
+ ```
86
+
87
+ AgentDoc also ships with an Agent Skill at [`skills/agent-doc-system/`](skills/agent-doc-system/SKILL.md).
88
+
89
+ ## Documentation
90
+
91
+ - [Getting started & migration](docs/MIGRATION.md)
92
+ - [Authority & conflicts](docs/AUTHORITY.md)
93
+ - [CI & verification](docs/CI.md)
94
+ - [Specification](docs/SPEC.md)
95
+ - [Agent Skill](skills/agent-doc-system/SKILL.md)
96
+
97
+ ## License
98
+
99
+ [MIT](LICENSE) © 2026 lbframe
@@ -0,0 +1,306 @@
1
+ // Ansible adapter: what host provisioning actually installs.
2
+ //
3
+ // Reads the playbooks under a unit root without running anything and without
4
+ // evaluating a single Jinja expression. From every task reachable from a
5
+ // playbook (plays, roles, includes, blocks) it records the host paths the
6
+ // task creates and the services it manages. That inventory is the DERIVED side
7
+ // of a normative requirement whose `satisfiedBy` names this adapter and whose
8
+ // `expects` names a path or a service:
9
+ //
10
+ // provided a reachable, unconditional task installs it deterministic
11
+ // absent no reachable task does, and every task is transparent deterministic
12
+ //
13
+ // Anything weaker is a candidate, which the authority engine never elects: a
14
+ // matching task behind a `when`, or an absence claimed over a tree that holds
15
+ // an opaque task (a shell command, a package install, a templated path, a role
16
+ // that is not in the repository). An absence is only a fact when the adapter
17
+ // could have seen the opposite.
18
+ import { Adapter } from "./registry.mjs";
19
+ import { parseYamlDocuments } from "../core/yaml.mjs";
20
+ import { AgentDocError } from "../core/codes.mjs";
21
+
22
+ // Modules that create the path they name, and the arguments naming it.
23
+ const PATH_MODULES = {
24
+ copy: ["dest"],
25
+ template: ["dest"],
26
+ unarchive: ["dest", "creates"],
27
+ get_url: ["dest"],
28
+ git: ["dest"],
29
+ synchronize: ["dest"],
30
+ pip: ["virtualenv"],
31
+ };
32
+ // `file` creates only for these states; `lineinfile`/`blockinfile` only with
33
+ // `create: true`.
34
+ const FILE_CREATING_STATES = new Set(["directory", "touch", "link", "hard"]);
35
+ const SERVICE_MODULES = new Set(["systemd", "systemd_service", "service"]);
36
+ // Commands can do anything; their `creates` is the one thing they promise.
37
+ const COMMAND_MODULES = new Set(["command", "shell", "script", "raw"]);
38
+ // Modules that change no file the adapter would be asked about.
39
+ const INERT_MODULES = new Set([
40
+ "debug", "set_fact", "assert", "fail", "meta", "pause", "wait_for", "stat", "ping", "setup",
41
+ "include_vars", "group_by", "add_host", "user", "group", "uri", "slurp", "fetch", "set_stats",
42
+ ]);
43
+ // Task keywords, i.e. everything on a task that is not its module.
44
+ const TASK_KEYWORDS = new Set([
45
+ "name", "when", "loop", "with_items", "with_dict", "with_fileglob", "loop_control", "register", "notify",
46
+ "become", "become_user", "become_method", "tags", "vars", "environment", "args", "ignore_errors", "changed_when",
47
+ "failed_when", "until", "retries", "delay", "no_log", "delegate_to", "run_once", "check_mode", "diff",
48
+ "listen", "timeout", "throttle", "any_errors_fatal", "module_defaults", "collections", "connection",
49
+ ]);
50
+ const INCLUDES = new Set(["include_tasks", "import_tasks", "include"]);
51
+ const ROLE_INCLUDES = new Set(["include_role", "import_role"]);
52
+
53
+ export class AnsibleAdapter extends Adapter {
54
+ static adapterName = "ansible";
55
+ constructor() {
56
+ super({ name: "ansible", version: "1.0.0", kind: "source" });
57
+ }
58
+
59
+ // A unit that holds a playbook is a deployable infrastructure unit.
60
+ detect(ctx, root) {
61
+ const pb = playbooks(ctx.repo, root);
62
+ return pb.length ? { path: pb[0].path, shape: "deployable" } : null;
63
+ }
64
+
65
+ extract(ctx, unit) {
66
+ const inv = inventory(ctx.repo, unit.root);
67
+ if (!inv.files.length) return {};
68
+ for (const r of ctx.requirements || []) {
69
+ if (r.subject !== ctx.compRef || !r.satisfiedBy.includes("ansible") || !r.expects) continue;
70
+ const verdict = judge(inv, r.expects);
71
+ const files = verdict.files.length ? verdict.files : inv.files;
72
+ ctx.addFact(r.subject, r.key, verdict.value, {
73
+ evidenceClass: "DERIVED",
74
+ confidence: verdict.certain ? "deterministic" : "candidate",
75
+ provRecs: files.map((f) => ctx.prov.add("observed", f, "ansible-extractor", ["ass:pending"])),
76
+ semantics: "ansible: " + verdict.why,
77
+ });
78
+ }
79
+ return {
80
+ provisioning: {
81
+ installs: inv.installs.map((i) => ({ path: i.path, file: i.file, task: i.task, conditional: i.conditional })),
82
+ services: inv.services.map((s) => ({ name: s.name, file: s.file, task: s.task, conditional: s.conditional })),
83
+ opaque: inv.opaque,
84
+ },
85
+ };
86
+ }
87
+ }
88
+
89
+ function judge(inv, expects) {
90
+ const target = expects.installs || expects.service;
91
+ const what = expects.installs ? "path " + target : "service " + target;
92
+ const hits = expects.installs
93
+ ? inv.installs.filter((i) => i.path === target || i.path.startsWith(target + "/"))
94
+ : inv.services.filter((s) => s.name === target || s.name === target + ".service");
95
+ const sure = hits.filter((h) => !h.conditional);
96
+ if (sure.length) {
97
+ return { value: "provided", certain: true, files: uniq(sure.map((h) => h.file)), why: what + " installed by " + describe(sure[0]) };
98
+ }
99
+ if (hits.length) {
100
+ return { value: "provided", certain: false, files: uniq(hits.map((h) => h.file)), why: what + " installed only conditionally by " + describe(hits[0]) };
101
+ }
102
+ if (inv.opaque.length) {
103
+ return {
104
+ value: "absent", certain: false, files: [],
105
+ why: "no task installs " + what + ", but absence cannot be proven: " + inv.opaque.slice(0, 3).join("; ") +
106
+ (inv.opaque.length > 3 ? " (+" + (inv.opaque.length - 3) + " more)" : ""),
107
+ };
108
+ }
109
+ return { value: "absent", certain: true, files: [], why: "no reachable task installs " + what };
110
+ }
111
+
112
+ function describe(h) {
113
+ return h.file + (h.task ? " (" + h.task + ")" : "");
114
+ }
115
+
116
+ function uniq(a) {
117
+ return [...new Set(a)].sort();
118
+ }
119
+
120
+ function parse(repo, file) {
121
+ try {
122
+ return parseYamlDocuments(repo.readText(file), file, { allowSequenceRoot: true }).map((d) => d.doc);
123
+ } catch (e) {
124
+ if (e instanceof AgentDocError) return null;
125
+ throw e;
126
+ }
127
+ }
128
+
129
+ function isYaml(f) {
130
+ return /\.ya?ml$/.test(f);
131
+ }
132
+
133
+ function playbooks(repo, root) {
134
+ const out = [];
135
+ for (const f of repo.walk(root)) {
136
+ if (!isYaml(f) || /(^|\/)(tasks|handlers|vars|defaults|meta|group_vars|host_vars|templates|files)\//.test(f.slice(root.length))) continue;
137
+ const docs = parse(repo, f);
138
+ if (!docs) continue;
139
+ const plays = docs.flatMap((d) => (Array.isArray(d) ? d : []));
140
+ if (plays.length && plays.every((p) => p && typeof p === "object" && (p.hosts !== undefined || p.import_playbook !== undefined))) {
141
+ out.push({ path: f, plays });
142
+ }
143
+ }
144
+ return out.sort((a, b) => (a.path < b.path ? -1 : 1));
145
+ }
146
+
147
+ // Every task reachable from a playbook under `root`, walked in a fixed order.
148
+ function inventory(repo, root) {
149
+ const inv = { files: new Set(), installs: [], services: [], opaque: [] };
150
+ const seen = new Set();
151
+ const opaque = (file, task, why) => inv.opaque.push(file + (task ? " (" + task + ")" : "") + ": " + why);
152
+
153
+ const dirOf = (p) => p.split("/").slice(0, -1).join("/");
154
+ const join = (dir, rel) => {
155
+ const parts = (dir ? dir.split("/") : []).concat(rel.split("/"));
156
+ const out = [];
157
+ for (const s of parts) {
158
+ if (s === "" || s === ".") continue;
159
+ if (s === "..") { if (!out.length) return null; out.pop(); } else out.push(s);
160
+ }
161
+ return out.join("/");
162
+ };
163
+ const templated = (v) => typeof v === "string" && /\{\{|\{%/.test(v);
164
+
165
+ const findRole = (name, fromDir) => {
166
+ for (const base of [fromDir, root]) {
167
+ for (const ext of ["yml", "yaml"]) {
168
+ const p = join(base, "roles/" + name + "/tasks/main." + ext);
169
+ if (p && repo.exists(p)) return p;
170
+ }
171
+ }
172
+ return null;
173
+ };
174
+
175
+ const walkFile = (file, conditional, kind) => {
176
+ const key = file + "|" + conditional + "|" + kind;
177
+ if (seen.has(key)) return;
178
+ seen.add(key);
179
+ inv.files.add(file);
180
+ const docs = parse(repo, file);
181
+ if (!docs) { opaque(file, null, "not parseable as YAML"); return; }
182
+ const list = docs.flatMap((d) => (Array.isArray(d) ? d : []));
183
+ if (kind === "playbook") for (const p of list) walkPlay(file, p, conditional);
184
+ else walkTasks(file, list, conditional);
185
+ };
186
+
187
+ const walkPlay = (file, play, conditional) => {
188
+ if (!play || typeof play !== "object") return;
189
+ if (play.import_playbook !== undefined) {
190
+ const ref = String(play.import_playbook);
191
+ const p = !templated(ref) && join(dirOf(file), ref);
192
+ if (p && repo.exists(p)) walkFile(p, conditional || play.when !== undefined, "playbook");
193
+ else opaque(file, null, "imported playbook not resolvable in the repository: " + ref);
194
+ return;
195
+ }
196
+ const cond = conditional || play.when !== undefined;
197
+ for (const section of ["pre_tasks", "roles", "tasks", "post_tasks", "handlers"]) {
198
+ const items = play[section];
199
+ if (!Array.isArray(items)) continue;
200
+ if (section === "roles") {
201
+ for (const r of items) {
202
+ const name = typeof r === "string" ? r : r && (r.role || r.name);
203
+ walkRole(file, String(name), cond || (r && typeof r === "object" && r.when !== undefined));
204
+ }
205
+ } else {
206
+ walkTasks(file, items, cond);
207
+ }
208
+ }
209
+ };
210
+
211
+ const walkRole = (file, name, conditional) => {
212
+ const main = !templated(name) && findRole(name, dirOf(file));
213
+ if (main) walkFile(main, conditional, "tasks");
214
+ else opaque(file, null, "role not found in the repository: " + name);
215
+ };
216
+
217
+ const walkTasks = (file, tasks, conditional) => {
218
+ for (const t of tasks) {
219
+ if (!t || typeof t !== "object" || Array.isArray(t)) continue;
220
+ const cond = conditional || t.when !== undefined;
221
+ const label = typeof t.name === "string" ? t.name : null;
222
+ if (Array.isArray(t.block)) {
223
+ walkTasks(file, t.block, cond);
224
+ // A rescue runs only when the block failed: never an unconditional install.
225
+ for (const k of ["rescue", "always"]) if (Array.isArray(t[k])) walkTasks(file, t[k], cond || k === "rescue");
226
+ continue;
227
+ }
228
+ const modKey = Object.keys(t).find((k) => !TASK_KEYWORDS.has(k));
229
+ if (!modKey) continue;
230
+ const mod = modKey.split(".").pop();
231
+ const raw = t[modKey];
232
+ const args = { ...parseFreeForm(raw), ...(t.args && typeof t.args === "object" ? t.args : {}) };
233
+ // A loop runs the module once per item with a templated argument; the
234
+ // adapter does not expand it.
235
+ const looped = t.loop !== undefined || Object.keys(t).some((k) => k.startsWith("with_"));
236
+
237
+ if (INCLUDES.has(mod)) {
238
+ const ref = typeof raw === "string" ? raw.split(/\s+/)[0] : args.file;
239
+ const p = ref && !templated(ref) && join(dirOf(file), String(ref));
240
+ if (p && repo.exists(p)) walkFile(p, cond, "tasks");
241
+ else opaque(file, label, "included task file not resolvable in the repository: " + ref);
242
+ continue;
243
+ }
244
+ if (ROLE_INCLUDES.has(mod)) {
245
+ walkRole(file, String(args.name), cond);
246
+ continue;
247
+ }
248
+ const record = (list, entry) => list.push({ ...entry, file, task: label, conditional: cond });
249
+ if (PATH_MODULES[mod] || mod === "file" || mod === "lineinfile" || mod === "blockinfile") {
250
+ let names = PATH_MODULES[mod] || ["path", "dest", "name"];
251
+ if (mod === "file" && !FILE_CREATING_STATES.has(String(args.state || "file"))) names = [];
252
+ if ((mod === "lineinfile" || mod === "blockinfile") && !isTrue(args.create)) names = [];
253
+ for (const n of names) {
254
+ const v = args[n];
255
+ if (v === undefined) continue;
256
+ if (templated(v) || looped) { opaque(file, label, mod + " " + n + " is templated: " + v); continue; }
257
+ record(inv.installs, { path: normalisePath(String(v)) });
258
+ }
259
+ continue;
260
+ }
261
+ if (SERVICE_MODULES.has(mod)) {
262
+ const n = args.name;
263
+ if (n === undefined) continue;
264
+ if (templated(n) || looped) opaque(file, label, mod + " name is templated: " + n);
265
+ else record(inv.services, { name: String(n) });
266
+ continue;
267
+ }
268
+ if (COMMAND_MODULES.has(mod)) {
269
+ if (args.creates !== undefined && !templated(args.creates) && !looped) {
270
+ record(inv.installs, { path: normalisePath(String(args.creates)) });
271
+ } else {
272
+ opaque(file, label, mod + " task without a literal `creates`");
273
+ }
274
+ continue;
275
+ }
276
+ if (INERT_MODULES.has(mod)) continue;
277
+ opaque(file, label, "module " + modKey + " is not modeled and may change the host");
278
+ }
279
+ };
280
+
281
+ for (const pb of playbooks(repo, root)) walkFile(pb.path, false, "playbook");
282
+ const byPath = (a, b) => (a.path + a.file < b.path + b.file ? -1 : a.path + a.file > b.path + b.file ? 1 : 0);
283
+ return {
284
+ files: [...inv.files].sort(),
285
+ installs: inv.installs.sort(byPath),
286
+ services: inv.services.sort((a, b) => (a.name + a.file < b.name + b.file ? -1 : 1)),
287
+ opaque: [...new Set(inv.opaque)].sort(),
288
+ };
289
+ }
290
+
291
+ // `file: path=/a state=directory` — the key=value shorthand.
292
+ function parseFreeForm(raw) {
293
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
294
+ if (typeof raw !== "string") return {};
295
+ const out = {};
296
+ for (const m of raw.matchAll(/(\w+)=("[^"]*"|'[^']*'|\S+)/g)) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
297
+ return out;
298
+ }
299
+
300
+ function isTrue(v) {
301
+ return v === true || /^(yes|true)$/i.test(String(v));
302
+ }
303
+
304
+ function normalisePath(p) {
305
+ return p.length > 1 ? p.replace(/\/+$/, "") : p;
306
+ }
@@ -0,0 +1,27 @@
1
+ // Cache adapter: Redis / in-memory cache client evidence as a derived binding.
2
+ import { Adapter } from "./registry.mjs";
3
+
4
+ const CACHE_HINT = /(@upstash\/redis|ioredis|redis|go-redis|redis\.go|valkey|memcache|@memcached)/i;
5
+
6
+ export class CacheAdapter extends Adapter {
7
+ static adapterName = "cache";
8
+ constructor() {
9
+ super({ name: "cache", version: "1.0.0", kind: "source" });
10
+ }
11
+ extract(ctx, unit) {
12
+ const { repo, prov, compRef } = ctx;
13
+ const evFile = unit.goModule ? unit.root + "/go.mod" : unit.root + "/package.json";
14
+ if (!repo.exists(evFile)) return {};
15
+ const text = repo.readText(evFile);
16
+ if (!CACHE_HINT.test(text)) return {};
17
+ const pr = prov.add("observed", evFile, "binding-extractor", ["der:" + compRef + ":/bindings"]);
18
+ const resources = ctx.sources.entities.filter((e) => e.kind === "Resource" && e.doc.spec.type === "cache");
19
+ if (resources.length === 1) {
20
+ const client = ctx.clientKeyForResourceType("cache");
21
+ const attrs = client ? { via: "cache-client", client } : { via: "cache-client" };
22
+ ctx.addEdge("usesResource", compRef, resources[0].ref, attrs, [pr], "DERIVED");
23
+ return { bindings: [{ kind: "cache", logicalResourceRef: resources[0].ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }] };
24
+ }
25
+ return { bindings: [{ kind: "cache", resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }] };
26
+ }
27
+ }
@@ -0,0 +1,56 @@
1
+ // Shared contract-consumer resolution.
2
+ //
3
+ // A cross-component boundary is a contract plus its consumers. Consumers are
4
+ // discovered deterministically: a unit that references the canonical contract
5
+ // file from a source or build-configuration file, other than the provider's own
6
+ // unit, consumes that contract. Documentation mentions are excluded so a doc
7
+ // that quotes a contract path cannot manufacture a dependency edge.
8
+ import path from "node:path";
9
+
10
+ const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts|mjs|cjs|go|py|rb|java|kt|rs|ex|exs|cs|php|swift|gradle|proto|graphql|sql)$/;
11
+ const CONFIG_NAMES = new Set([
12
+ "package.json", "go.mod", "buf.gen.yaml", "buf.yaml", "buf.work.yaml",
13
+ "codegen.yaml", "codegen.yml", "openapi-ts.config.ts", "openapi-ts.config.js",
14
+ "oapi-codegen.yaml", "oapi-codegen.yml", "turbo.json", "Makefile", "justfile",
15
+ "Taskfile.yml", "build.gradle", "build.gradle.kts", "pom.xml",
16
+ ]);
17
+ const DOC_EXT = /\.(md|mdx|txt|adoc|rst)$/;
18
+
19
+ export function contractReferenceSites(repo, unit) {
20
+ const out = new Map(); // file -> true
21
+ for (const f of repo.walk(unit.root)) {
22
+ const base = path.posix.basename(f);
23
+ if (DOC_EXT.test(f)) continue;
24
+ if (f.includes("/docs/") || f.startsWith("docs/")) continue;
25
+ if (!CODE_EXT.test(f) && !CONFIG_NAMES.has(base)) continue;
26
+ out.set(f, true);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // Resolve consumers for a contract file. Returns Map<componentRef, string[]>.
32
+ // The per-unit file inventory is built once and cached on the context, so a
33
+ // repository with many contracts does not re-walk every unit per contract.
34
+ export function resolveContractConsumers(ctx, contractRef, providerRef) {
35
+ if (!ctx.referenceIndex) {
36
+ const perUnit = new Map();
37
+ for (const unit of ctx.discovery.eligible) {
38
+ if (!unit.component) continue;
39
+ perUnit.set(unit.component.ref, [...contractReferenceSites(ctx.repo, unit).keys()].sort());
40
+ }
41
+ ctx.referenceIndex = perUnit;
42
+ }
43
+ const out = new Map();
44
+ for (const [ref, files] of ctx.referenceIndex) {
45
+ if (ref === providerRef) continue;
46
+ const hits = files.filter((f) => ctx.repo.readText(f).includes(contractRef));
47
+ if (hits.length) out.set(ref, hits);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // A unit that names the contract in its own build config is a stronger signal
53
+ // than a stray mention in source.
54
+ export function isBuildConfig(file) {
55
+ return CONFIG_NAMES.has(path.posix.basename(file));
56
+ }