@intentius/chant 0.100.0 → 0.101.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.
- package/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/build-options.d.ts +2 -0
- package/dist/cli/build-options.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +8 -0
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon.d.ts +24 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-digest.d.ts +26 -5
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lint/config.d.ts +4 -4
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +2 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/serializer.d.ts +8 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/telemetry-attribution.d.ts +77 -0
- package/dist/telemetry-attribution.d.ts.map +1 -0
- package/dist/workspace/agent-cli.d.ts +83 -0
- package/dist/workspace/agent-cli.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes.d.ts +8 -1
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/checks/links.d.ts +1 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/live.d.ts +40 -0
- package/dist/workspace/checks/live.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +21 -2
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +63 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +1 -1
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +32 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +138 -3
- package/dist/workspace/export-cli.d.ts +12 -0
- package/dist/workspace/export-cli.d.ts.map +1 -0
- package/dist/workspace/export.d.ts +145 -0
- package/dist/workspace/export.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/import.d.ts +73 -0
- package/dist/workspace/import.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +6 -2
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
- package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-adopt.d.ts +106 -0
- package/dist/workspace/lineage-adopt.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +9 -2
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-cli.d.ts +6 -1
- package/dist/workspace/lineage-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-hash-index.d.ts +110 -0
- package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +10 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +147 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.d.ts +15 -3
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-provenance.d.ts +18 -0
- package/dist/workspace/lineage-provenance.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +30 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-versions.d.ts +116 -0
- package/dist/workspace/lineage-versions.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +31 -5
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/ls-generated.d.ts +37 -0
- package/dist/workspace/ls-generated.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/nested-graph.d.ts +56 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -0
- package/dist/workspace/nesting.d.ts +21 -0
- package/dist/workspace/nesting.d.ts.map +1 -0
- package/dist/workspace/pin-cli.d.ts +10 -0
- package/dist/workspace/pin-cli.d.ts.map +1 -0
- package/dist/workspace/pin-integrity.d.ts +51 -0
- package/dist/workspace/pin-integrity.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +26 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +7 -11
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +30 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +5 -2
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +22 -4
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +43 -5
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/returns.d.ts +129 -0
- package/dist/workspace/returns.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/template-manifest.d.ts +11 -3
- package/dist/workspace/template-manifest.d.ts.map +1 -1
- package/dist/workspace/trust/attestor.d.ts +8 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -1
- package/dist/workspace/trust/dsse.d.ts +58 -0
- package/dist/workspace/trust/dsse.d.ts.map +1 -0
- package/dist/workspace/trust/evidence-cli.d.ts +66 -0
- package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
- package/dist/workspace/trust/evidence.d.ts +93 -0
- package/dist/workspace/trust/evidence.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +54 -1
- package/dist/workspace/trust/policy.d.ts.map +1 -1
- package/dist/workspace/trust/provenance.d.ts +21 -1
- package/dist/workspace/trust/provenance.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.d.ts +132 -0
- package/dist/workspace/trust/rotation.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/signers-cli.d.ts +49 -0
- package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +12 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.d.ts +13 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -1
- package/dist/workspace/trust/verify.d.ts +15 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/write-scope.d.ts +199 -0
- package/dist/workspace/write-scope.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/build.ts +8 -0
- package/src/cli/build-options.ts +4 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/import-live.test.ts +69 -1
- package/src/cli/commands/import.ts +48 -22
- package/src/cli/handlers/fan-out.test.ts +6 -6
- package/src/cli/handlers/fan-out.ts +2 -1
- package/src/cli/handlers/graph.test.ts +42 -0
- package/src/cli/handlers/graph.ts +22 -0
- package/src/cli/handlers/operator.ts +1 -1
- package/src/cli/main.test.ts +32 -0
- package/src/cli/main.ts +103 -6
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +33 -2
- package/src/cli/registry.ts +22 -0
- package/src/cli/serve-mcp-workspace.test.ts +1 -1
- package/src/codegen/release-wiring.test.ts +5 -1
- package/src/components/fan-out-output.test.ts +1 -1
- package/src/components/fan-out.test.ts +1 -1
- package/src/components/promote.test.ts +1 -1
- package/src/config.ts +12 -0
- package/src/content-digest.test.ts +2 -2
- package/src/lexicon.ts +25 -1
- package/src/lifecycle/gate-ledger.test.ts +14 -0
- package/src/lifecycle/gate-ledger.ts +2 -1
- package/src/lifecycle/plan-digest.test.ts +54 -3
- package/src/lifecycle/plan-digest.ts +38 -8
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/propose-upgrade.ts +8 -5
- package/src/op/gate-approval.test.ts +17 -0
- package/src/op/gate.ts +3 -3
- package/src/op/index.ts +1 -1
- package/src/serializer.ts +9 -0
- package/src/telemetry-attribution.test.ts +91 -0
- package/src/telemetry-attribution.ts +145 -0
- package/src/workspace/agent-cli.ts +134 -0
- package/src/workspace/agent.schema.json +356 -0
- package/src/workspace/behold-kinds.test.ts +1 -1
- package/src/workspace/changes-cli.ts +5 -0
- package/src/workspace/changes.schema.json +179 -1
- package/src/workspace/changes.ts +58 -4
- package/src/workspace/check-live.test.ts +192 -0
- package/src/workspace/check.schema.json +64 -0
- package/src/workspace/checks/links.ts +23 -2
- package/src/workspace/checks/live.ts +113 -0
- package/src/workspace/checks.test.ts +2 -2
- package/src/workspace/checks.ts +20 -3
- package/src/workspace/compose-graph.test.ts +47 -0
- package/src/workspace/compose-graph.ts +120 -3
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
- package/src/workspace/declaration.schema.json +138 -3
- package/src/workspace/declaration.ts +106 -1
- package/src/workspace/declared-kinds.test.ts +33 -0
- package/src/workspace/evidence.schema.json +279 -0
- package/src/workspace/export-cli.ts +180 -0
- package/src/workspace/export-import.test.ts +282 -0
- package/src/workspace/export.ts +486 -0
- package/src/workspace/graph-cli.ts +47 -1
- package/src/workspace/graph-contract.test.ts +89 -4
- package/src/workspace/graph.schema.json +206 -1
- package/src/workspace/import.ts +325 -0
- package/src/workspace/kinds.test.ts +5 -5
- package/src/workspace/kinds.ts +25 -3
- package/src/workspace/lineage-adopt-cli.ts +103 -0
- package/src/workspace/lineage-adopt.test.ts +552 -0
- package/src/workspace/lineage-adopt.ts +452 -0
- package/src/workspace/lineage-check.ts +26 -5
- package/src/workspace/lineage-cli.ts +12 -2
- package/src/workspace/lineage-hash-index.ts +305 -0
- package/src/workspace/lineage-init.test.ts +9 -0
- package/src/workspace/lineage-init.ts +29 -9
- package/src/workspace/lineage-lock.ts +54 -0
- package/src/workspace/lineage-migrations.test.ts +27 -0
- package/src/workspace/lineage-migrations.ts +44 -14
- package/src/workspace/lineage-provenance.ts +40 -0
- package/src/workspace/lineage-upgrade-cli.ts +9 -5
- package/src/workspace/lineage-upgrade.test.ts +92 -1
- package/src/workspace/lineage-upgrade.ts +111 -16
- package/src/workspace/lineage-versions.ts +348 -0
- package/src/workspace/links.test.ts +121 -2
- package/src/workspace/links.ts +97 -6
- package/src/workspace/ls-contract.test.ts +84 -1
- package/src/workspace/ls-generated.ts +111 -0
- package/src/workspace/ls.schema.json +19 -0
- package/src/workspace/ls.ts +8 -1
- package/src/workspace/member-commands.ts +3 -1
- package/src/workspace/nested-graph.test.ts +176 -0
- package/src/workspace/nested-graph.ts +169 -0
- package/src/workspace/nesting.ts +37 -0
- package/src/workspace/pin-cli.test.ts +71 -0
- package/src/workspace/pin-cli.ts +57 -0
- package/src/workspace/pin-integrity.test.ts +121 -0
- package/src/workspace/pin-integrity.ts +104 -0
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/read-contract.test.ts +24 -0
- package/src/workspace/reason-codes.test.ts +10 -0
- package/src/workspace/reason-codes.ts +29 -2
- package/src/workspace/record-sessions.ts +12 -14
- package/src/workspace/records-amend.schema.json +4 -0
- package/src/workspace/records-cli.ts +93 -12
- package/src/workspace/records-close.schema.json +6 -2
- package/src/workspace/records-close.ts +10 -2
- package/src/workspace/records-formats.test.ts +13 -5
- package/src/workspace/records-new.schema.json +4 -0
- package/src/workspace/records-review.schema.json +4 -0
- package/src/workspace/records-sessions.test.ts +23 -6
- package/src/workspace/records-write.test.ts +65 -8
- package/src/workspace/records-write.ts +70 -6
- package/src/workspace/records.schema.json +55 -3
- package/src/workspace/records.ts +96 -5
- package/src/workspace/returns.ts +328 -0
- package/src/workspace/signers.schema.json +206 -0
- package/src/workspace/status-gates.ts +2 -1
- package/src/workspace/template-manifest.ts +22 -4
- package/src/workspace/trust/attestor.ts +15 -0
- package/src/workspace/trust/dsse.ts +134 -0
- package/src/workspace/trust/evidence-cli.ts +195 -0
- package/src/workspace/trust/evidence.test.ts +241 -0
- package/src/workspace/trust/evidence.ts +207 -0
- package/src/workspace/trust/policy.ts +110 -3
- package/src/workspace/trust/provenance.ts +41 -4
- package/src/workspace/trust/record-seal.test.ts +1 -1
- package/src/workspace/trust/rotation.test.ts +258 -0
- package/src/workspace/trust/rotation.ts +336 -0
- package/src/workspace/trust/seal.ts +11 -0
- package/src/workspace/trust/signers-cli.ts +178 -0
- package/src/workspace/trust/ssh-commit.ts +53 -4
- package/src/workspace/trust/test-repo.ts +18 -0
- package/src/workspace/trust/trust.test.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +1 -0
- package/src/workspace/trust/verify.ts +22 -0
- package/src/workspace/work-evidence.schema.json +4 -0
- package/src/workspace/write-scope.test.ts +340 -0
- package/src/workspace/write-scope.ts +448 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace versions [<dir>] [--template <id>] [--json]` (#2551, D9,
|
|
3
|
+
* requirement T9).
|
|
4
|
+
*
|
|
5
|
+
* Reports, for a family of workspaces, which template version and which chant
|
|
6
|
+
* and lexicon versions each one is on. A family is the set of workspaces made
|
|
7
|
+
* from the same template: each lineage scope belongs to the family of its
|
|
8
|
+
* template id. The command finds every `.chant/workspace.lock.json` under
|
|
9
|
+
* `<dir>` (the working directory by default), so pointing it at a directory of
|
|
10
|
+
* checkouts compares them all. Without `--available` nothing is fetched and no
|
|
11
|
+
* code runs: it reads lock files and `package.json` files only.
|
|
12
|
+
*
|
|
13
|
+
* With `--available`, each family whose template is a git source also lists
|
|
14
|
+
* the template's version tags (`git ls-remote --tags`, catalogued in
|
|
15
|
+
* `test/egress-catalogue.ts`), and each member says how many are newer than
|
|
16
|
+
* the version its lock sits at. That is the list `chant workspace upgrade
|
|
17
|
+
* --to <tag>` chooses from.
|
|
18
|
+
*
|
|
19
|
+
* Per D9 the word "drift" keeps its existing meanings, so a workspace on an
|
|
20
|
+
* older version than the newest one in its family is reported as `behind`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
import { formatError } from "../cli/format";
|
|
26
|
+
import type { CommandContext } from "../cli/registry";
|
|
27
|
+
import { TemplateTags } from "./lineage-hash-index";
|
|
28
|
+
import { LockError, LOCK_FILE, readLock, type LineageSource } from "./lineage-lock";
|
|
29
|
+
import { lineageProvenance } from "./lineage-provenance";
|
|
30
|
+
import { compareVersions, formatVersion, parseVersion } from "./lineage-version";
|
|
31
|
+
|
|
32
|
+
/** How deep under `<dir>` a lock is looked for. */
|
|
33
|
+
export const MAX_DEPTH = 8;
|
|
34
|
+
|
|
35
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".chant"]);
|
|
36
|
+
|
|
37
|
+
export interface ScopeVersion {
|
|
38
|
+
scope: string;
|
|
39
|
+
kind: "template" | "vendor";
|
|
40
|
+
template: string;
|
|
41
|
+
ref: string | null;
|
|
42
|
+
/** The version the ref names, or null when it names none (a branch, a commit). */
|
|
43
|
+
version: string | null;
|
|
44
|
+
commit: string | null;
|
|
45
|
+
migrations: number;
|
|
46
|
+
/** `files` or `lineage` when adopt-lineage recorded the lineage (#2551), otherwise null. */
|
|
47
|
+
adoption: "files" | "lineage" | null;
|
|
48
|
+
/** D5's level: `adopted` once the policy at base admits the adopted range, `unattested` otherwise. */
|
|
49
|
+
provenance: "adopted" | "unattested";
|
|
50
|
+
/** Where the template is fetched from: the source type, and for git the URL as the lock records it. */
|
|
51
|
+
source: { type: LineageSource["type"]; url?: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PluginVersion {
|
|
55
|
+
name: string;
|
|
56
|
+
/** The range `package.json` declares. */
|
|
57
|
+
declared: string;
|
|
58
|
+
/** The version installed where the workspace resolves it, or null when it is not installed. */
|
|
59
|
+
installed: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WorkspaceVersions {
|
|
63
|
+
/** Relative to `<dir>`, `.` for `<dir>` itself. */
|
|
64
|
+
path: string;
|
|
65
|
+
scopes: ScopeVersion[];
|
|
66
|
+
plugins: PluginVersion[];
|
|
67
|
+
/** Set when the lock could not be read; the workspace then has no scopes. */
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FamilyMember {
|
|
72
|
+
path: string;
|
|
73
|
+
scope: string;
|
|
74
|
+
ref: string | null;
|
|
75
|
+
version: string | null;
|
|
76
|
+
/** Older than the newest version in the family. */
|
|
77
|
+
behind: boolean;
|
|
78
|
+
/** With `--available`: how many of the template's version tags are newer than this member's version, or null when unknown. */
|
|
79
|
+
newer?: number | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface AvailableVersions {
|
|
83
|
+
/** The template's version tags, oldest first, narrowed by `--tags`. */
|
|
84
|
+
tags: string[];
|
|
85
|
+
/** The newest of them, or null when there are none. */
|
|
86
|
+
newest: string | null;
|
|
87
|
+
/** Why the tags could not be listed (not a git template, or the fetch failed). */
|
|
88
|
+
error?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface Family {
|
|
92
|
+
template: string;
|
|
93
|
+
/** The newest version any member is on, or null when no member's ref is a version. */
|
|
94
|
+
newest: string | null;
|
|
95
|
+
members: FamilyMember[];
|
|
96
|
+
/** With `--available`: the template's own version tags. */
|
|
97
|
+
available?: AvailableVersions;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface VersionsReport {
|
|
101
|
+
root: string;
|
|
102
|
+
workspaces: WorkspaceVersions[];
|
|
103
|
+
families: Family[];
|
|
104
|
+
/** chant and lexicon packages that members of one family have at different installed versions. */
|
|
105
|
+
pluginSpread: Array<{ template: string; name: string; versions: Record<string, string[]> }>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Every directory under `dir` that holds a lineage lock, nearest first. */
|
|
109
|
+
export function findLocks(dir: string, depth = MAX_DEPTH): string[] {
|
|
110
|
+
const found: string[] = [];
|
|
111
|
+
const walk = (at: string, level: number): void => {
|
|
112
|
+
if (existsSync(join(at, LOCK_FILE))) found.push(at);
|
|
113
|
+
if (level >= depth) return;
|
|
114
|
+
let names: string[];
|
|
115
|
+
try {
|
|
116
|
+
names = readdirSync(at);
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const name of names.sort()) {
|
|
121
|
+
if (SKIP_DIRS.has(name) || (name.startsWith(".") && name !== ".")) continue;
|
|
122
|
+
const child = join(at, name);
|
|
123
|
+
try {
|
|
124
|
+
if (!statSync(child).isDirectory()) continue;
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
walk(child, level + 1);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
walk(dir, 0);
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isPlugin(name: string): boolean {
|
|
136
|
+
return name === "@intentius/chant" || name.startsWith("@intentius/chant-lexicon-");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The version of `pkg` that node would resolve from `dir`: the nearest `node_modules/<pkg>` going up. */
|
|
140
|
+
function installedVersion(pkg: string, dir: string): string | null {
|
|
141
|
+
let at = dir;
|
|
142
|
+
for (;;) {
|
|
143
|
+
const manifest = join(at, "node_modules", pkg, "package.json");
|
|
144
|
+
if (existsSync(manifest)) {
|
|
145
|
+
try {
|
|
146
|
+
return (JSON.parse(readFileSync(manifest, "utf-8")) as { version?: string }).version ?? null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const up = dirname(at);
|
|
152
|
+
if (up === at) return null;
|
|
153
|
+
at = up;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** chant and its lexicons, as the workspace's `package.json` declares them. */
|
|
158
|
+
export function pluginVersions(dir: string): PluginVersion[] {
|
|
159
|
+
const manifest = join(dir, "package.json");
|
|
160
|
+
if (!existsSync(manifest)) return [];
|
|
161
|
+
let json: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
162
|
+
try {
|
|
163
|
+
json = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const declared = { ...(json.devDependencies ?? {}), ...(json.dependencies ?? {}) };
|
|
168
|
+
return Object.keys(declared)
|
|
169
|
+
.filter(isPlugin)
|
|
170
|
+
.sort()
|
|
171
|
+
.map((name) => ({ name, declared: declared[name], installed: installedVersion(name, dir) }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface VersionsOptions {
|
|
175
|
+
/** Only this template's family. */
|
|
176
|
+
template?: string;
|
|
177
|
+
/** List each git template's version tags. */
|
|
178
|
+
available?: boolean;
|
|
179
|
+
/** Narrow those tags with a glob, such as `kit-v*`. */
|
|
180
|
+
tags?: string;
|
|
181
|
+
/** Lists a template's version tags; replaced in tests. */
|
|
182
|
+
listTags?: (url: string, member: string | undefined, pattern?: string) => string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The default lister: `git ls-remote --tags` through ./lineage-hash-index.ts. */
|
|
186
|
+
function remoteTags(url: string, member: string | undefined, pattern?: string): string[] {
|
|
187
|
+
const tags = new TemplateTags(url, member, url);
|
|
188
|
+
try {
|
|
189
|
+
return tags.listTags(pattern).map((t) => t.tag);
|
|
190
|
+
} finally {
|
|
191
|
+
tags.dispose();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Build the report for every workspace under `dir`, narrowed to one template's family when `template` is given. */
|
|
196
|
+
export function workspaceVersions(dir: string, templateOrOptions?: string | VersionsOptions): VersionsReport {
|
|
197
|
+
const options: VersionsOptions = typeof templateOrOptions === "string" ? { template: templateOrOptions } : (templateOrOptions ?? {});
|
|
198
|
+
const template = options.template;
|
|
199
|
+
const root = resolve(dir);
|
|
200
|
+
const workspaces: WorkspaceVersions[] = [];
|
|
201
|
+
for (const at of findLocks(root)) {
|
|
202
|
+
const path = relative(root, at).split(sep).join("/") || ".";
|
|
203
|
+
let scopes: ScopeVersion[] = [];
|
|
204
|
+
let error: string | undefined;
|
|
205
|
+
try {
|
|
206
|
+
const lock = readLock(at)!;
|
|
207
|
+
scopes = Object.entries(lock.scopes).map(([scope, l]) => {
|
|
208
|
+
const v = parseVersion(l.ref);
|
|
209
|
+
return {
|
|
210
|
+
scope,
|
|
211
|
+
kind: l.kind,
|
|
212
|
+
template: l.template,
|
|
213
|
+
ref: l.ref ?? null,
|
|
214
|
+
version: v ? formatVersion(v) : null,
|
|
215
|
+
commit: l.address?.commit ?? null,
|
|
216
|
+
migrations: l.migrations.length,
|
|
217
|
+
adoption: l.adoption?.by ?? null,
|
|
218
|
+
provenance: lineageProvenance(at, l)?.level ?? "unattested",
|
|
219
|
+
source: l.source.type === "git" ? { type: "git", url: l.source.url.startsWith(".") ? resolve(at, l.source.url) : l.source.url } : { type: l.source.type },
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!(err instanceof LockError)) throw err;
|
|
224
|
+
error = err.message;
|
|
225
|
+
}
|
|
226
|
+
if (template !== undefined && !error && !scopes.some((s) => s.template === template)) continue;
|
|
227
|
+
workspaces.push({ path, scopes, plugins: pluginVersions(at), ...(error ? { error } : {}) });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const byTemplate = new Map<string, FamilyMember[]>();
|
|
231
|
+
for (const w of workspaces) {
|
|
232
|
+
for (const s of w.scopes) {
|
|
233
|
+
if (template !== undefined && s.template !== template) continue;
|
|
234
|
+
const list = byTemplate.get(s.template) ?? [];
|
|
235
|
+
list.push({ path: w.path, scope: s.scope, ref: s.ref, version: s.version, behind: false });
|
|
236
|
+
byTemplate.set(s.template, list);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const families: Family[] = [];
|
|
240
|
+
const pluginSpread: VersionsReport["pluginSpread"] = [];
|
|
241
|
+
for (const [id, members] of [...byTemplate.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
242
|
+
const versions = members.map((m) => (m.version ? parseVersion(m.version)! : null)).filter((v) => v !== null);
|
|
243
|
+
const newest = versions.length > 0 ? versions.reduce((a, b) => (compareVersions(a, b) >= 0 ? a : b)) : null;
|
|
244
|
+
for (const m of members) {
|
|
245
|
+
m.behind = newest !== null && m.version !== null && compareVersions(parseVersion(m.version)!, newest) < 0;
|
|
246
|
+
}
|
|
247
|
+
const family: Family = { template: id, newest: newest ? formatVersion(newest) : null, members };
|
|
248
|
+
if (options.available) family.available = available(id, workspaces, members, options);
|
|
249
|
+
families.push(family);
|
|
250
|
+
|
|
251
|
+
const paths = new Set(members.map((m) => m.path));
|
|
252
|
+
const seen = new Map<string, Record<string, string[]>>();
|
|
253
|
+
for (const w of workspaces.filter((w) => paths.has(w.path))) {
|
|
254
|
+
for (const p of w.plugins) {
|
|
255
|
+
const v = p.installed ?? `not installed (${p.declared})`;
|
|
256
|
+
const entry = seen.get(p.name) ?? {};
|
|
257
|
+
(entry[v] ??= []).push(w.path);
|
|
258
|
+
seen.set(p.name, entry);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const [name, spread] of [...seen.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
262
|
+
if (Object.keys(spread).length > 1) pluginSpread.push({ template: id, name, versions: spread });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { root, workspaces, families, pluginSpread };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The version tags of a family's template, and how far behind them each member is. */
|
|
269
|
+
function available(id: string, workspaces: WorkspaceVersions[], members: FamilyMember[], options: VersionsOptions): AvailableVersions {
|
|
270
|
+
const scope = workspaces.flatMap((w) => w.scopes).find((s) => s.template === id);
|
|
271
|
+
if (!scope || scope.source.type !== "git" || !scope.source.url) {
|
|
272
|
+
for (const m of members) m.newer = null;
|
|
273
|
+
return { tags: [], newest: null, error: `a ${scope?.source.type ?? "unknown"} source has no version tags to list` };
|
|
274
|
+
}
|
|
275
|
+
const member = id.includes("#") ? id.slice(id.lastIndexOf("#") + 1) : undefined;
|
|
276
|
+
let tags: string[];
|
|
277
|
+
try {
|
|
278
|
+
tags = (options.listTags ?? remoteTags)(scope.source.url, member, options.tags);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
for (const m of members) m.newer = null;
|
|
281
|
+
return { tags: [], newest: null, error: err instanceof Error ? err.message : String(err) };
|
|
282
|
+
}
|
|
283
|
+
const versions = tags.map((t) => parseVersion(t)).filter((v) => v !== null);
|
|
284
|
+
for (const m of members) {
|
|
285
|
+
const v = m.version ? parseVersion(m.version) : null;
|
|
286
|
+
m.newer = v ? versions.filter((t) => compareVersions(t, v) > 0).length : null;
|
|
287
|
+
}
|
|
288
|
+
return { tags, newest: tags.length > 0 ? tags[tags.length - 1] : null };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The report as terminal lines. */
|
|
292
|
+
export function describeVersions(report: VersionsReport): string[] {
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
for (const w of report.workspaces) {
|
|
295
|
+
lines.push(w.path);
|
|
296
|
+
if (w.error) lines.push(` unreadable: ${w.error}`);
|
|
297
|
+
for (const s of w.scopes) {
|
|
298
|
+
const at = s.commit ? ` (${s.commit.slice(0, 12)})` : "";
|
|
299
|
+
const adopted = s.adoption === "files" && s.provenance !== "adopted" ? ", adopted range not yet at base" : s.adoption === "lineage" ? ", moved from a directory lineage" : "";
|
|
300
|
+
lines.push(` ${s.scope} ${s.template}@${s.ref ?? "(no ref)"}${at} ${s.provenance}${adopted}${s.migrations > 0 ? `, ${s.migrations} migration(s)` : ""}`);
|
|
301
|
+
}
|
|
302
|
+
if (w.plugins.length > 0) {
|
|
303
|
+
lines.push(` plugins: ${w.plugins.map((p) => `${p.name} ${p.installed ?? `not installed (${p.declared})`}`).join(", ")}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const f of report.families) {
|
|
307
|
+
const behind = f.members.filter((m) => m.behind);
|
|
308
|
+
lines.push(
|
|
309
|
+
`family ${f.template}: ${f.members.length} scope(s), newest ${f.newest ?? "(no versions)"}${behind.length > 0 ? `; behind: ${behind.map((m) => `${m.path}${m.scope === "." ? "" : `/${m.scope}`} (${m.ref})`).join(", ")}` : ""}`,
|
|
310
|
+
);
|
|
311
|
+
if (f.available) {
|
|
312
|
+
if (f.available.error) lines.push(` available: unknown (${f.available.error})`);
|
|
313
|
+
else {
|
|
314
|
+
lines.push(` available: ${f.available.tags.length} version tag(s), newest ${f.available.newest ?? "(none)"}`);
|
|
315
|
+
for (const m of f.members) {
|
|
316
|
+
if (m.newer === undefined || m.newer === null) continue;
|
|
317
|
+
lines.push(` ${m.path}${m.scope === "." ? "" : `/${m.scope}`} at ${m.ref}: ${m.newer === 0 ? "on the newest" : `${m.newer} newer`}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const s of report.pluginSpread.filter((p) => p.template === f.template)) {
|
|
322
|
+
lines.push(` ${s.name} differs: ${Object.entries(s.versions).map(([v, paths]) => `${v} in ${paths.join(", ")}`).join("; ")}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (report.workspaces.length === 0) lines.push(`no ${LOCK_FILE} under ${report.root}`);
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function runWorkspaceVersions(ctx: CommandContext): Promise<number> {
|
|
330
|
+
const { args } = ctx;
|
|
331
|
+
if (args.extraPositional2) {
|
|
332
|
+
console.error(formatError({ message: `unexpected argument: ${args.extraPositional2}`, hint: "chant workspace versions [<dir>] [--template <id>] [--available [--tags <glob>]] [--json]" }));
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
const dir = resolve(args.extraPositional ?? ".");
|
|
336
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
337
|
+
console.error(formatError({ message: `${dir} is not a directory` }));
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
const report = workspaceVersions(dir, {
|
|
341
|
+
template: args.template,
|
|
342
|
+
available: args.available,
|
|
343
|
+
tags: args.tags,
|
|
344
|
+
});
|
|
345
|
+
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
346
|
+
else console.log(describeVersions(report).join("\n"));
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
@@ -321,8 +321,8 @@ describe("the declaration's link fields", () => {
|
|
|
321
321
|
expect(d.members[0].outputs).toEqual(["One", "Two"]);
|
|
322
322
|
expect(d.members[1].outputs).toBeNull();
|
|
323
323
|
expect(d.members[1].links).toEqual([
|
|
324
|
-
{ member: "a", output: "One", kind: null, pointer: "/members/1/links/0" },
|
|
325
|
-
{ member: "a", output: "Two", kind: "output", pointer: "/members/1/links/1" },
|
|
324
|
+
{ member: "a", output: "One", kind: null, protocol: null, pointer: "/members/1/links/0" },
|
|
325
|
+
{ member: "a", output: "Two", kind: "output", protocol: null, pointer: "/members/1/links/1" },
|
|
326
326
|
]);
|
|
327
327
|
});
|
|
328
328
|
|
|
@@ -335,6 +335,125 @@ describe("the declaration's link fields", () => {
|
|
|
335
335
|
});
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
+
describe("telemetry links (#2558)", () => {
|
|
339
|
+
const collector = {
|
|
340
|
+
member: "ops",
|
|
341
|
+
pipelines: [
|
|
342
|
+
{ id: "traces", signal: "traces", receivers: ["otlp"], processors: [], exporters: ["otlphttp/backend"] },
|
|
343
|
+
{ id: "metrics", signal: "metrics", receivers: ["custom"], processors: [], exporters: ["otlphttp/backend"] },
|
|
344
|
+
],
|
|
345
|
+
components: [
|
|
346
|
+
{ id: "otlp", kind: "receiver", type: "otlp", endpoints: ["0.0.0.0:4318"], protocols: ["http/protobuf", "http/json"], pipelines: ["traces"] },
|
|
347
|
+
{ id: "custom", kind: "receiver", type: "custom", endpoints: [], protocols: [], pipelines: ["metrics"] },
|
|
348
|
+
{ id: "otlphttp/backend", kind: "exporter", type: "otlphttp", endpoints: ["https://b"], protocols: ["http/protobuf"], pipelines: ["traces", "metrics"] },
|
|
349
|
+
],
|
|
350
|
+
exporters: [{ id: "otlphttp/backend", type: "otlphttp", endpoints: ["https://b"], pipelines: ["traces", "metrics"], signals: ["traces", "metrics"] }],
|
|
351
|
+
};
|
|
352
|
+
const decl = (link: Record<string, unknown>) =>
|
|
353
|
+
parseDeclaration(
|
|
354
|
+
declaration([
|
|
355
|
+
{ name: "ops", dir: "ops", kind: "chant" },
|
|
356
|
+
{ name: "web", dir: "web", kind: "chant" },
|
|
357
|
+
{ name: "docs", dir: "docs", kind: "other", because: "prose" },
|
|
358
|
+
{ name: "app", dir: "app", kind: "other", because: "x", links: [{ kind: "telemetry", ...link }] },
|
|
359
|
+
]),
|
|
360
|
+
"chant.workspace.json",
|
|
361
|
+
);
|
|
362
|
+
const targets = (collectors: (typeof collector)[], composed = ["ops", "web"]) => ({
|
|
363
|
+
collectors: new Map(collectors.map((c) => [c.member, c])),
|
|
364
|
+
composed: new Set(composed),
|
|
365
|
+
});
|
|
366
|
+
const rowFor = (link: Record<string, unknown>, t?: ReturnType<typeof targets>) => resolveLinks(decl(link), new Map(), t)[0] as Extract<LinkTableRow, { producer: string }>;
|
|
367
|
+
|
|
368
|
+
test("resolves to a pipeline of the producer's collector, and says it is a pipeline", () => {
|
|
369
|
+
const row = rowFor({ member: "ops", output: "traces" }, targets([collector]));
|
|
370
|
+
expect(row).toMatchObject({ kind: "telemetry", status: "resolved", reason: null, target: "pipeline", output: "traces", producer: "ops" });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("resolves to an exporter when no pipeline has the name", () => {
|
|
374
|
+
expect(rowFor({ member: "ops", output: "otlphttp/backend" }, targets([collector]))).toMatchObject({ status: "resolved", target: "exporter" });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("is missing when the collector has no such pipeline or exporter, and lists what it has", () => {
|
|
378
|
+
const row = rowFor({ member: "ops", output: "logs" }, targets([collector]));
|
|
379
|
+
expect(row.status).toBe("missing");
|
|
380
|
+
expect(row.reason).toBe("ops's collector has no pipeline or exporter logs; pipelines: traces, metrics; exporters: otlphttp/backend");
|
|
381
|
+
expect(row.target).toBeUndefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("is missing when a composed producer declares no collector, and unresolved when it was not composed", () => {
|
|
385
|
+
expect(rowFor({ member: "web", output: "traces" }, targets([collector]))).toMatchObject({ status: "missing", reason: expect.stringContaining("web declares no collector") });
|
|
386
|
+
expect(rowFor({ member: "docs", output: "traces" }, targets([collector]))).toMatchObject({ status: "unresolved", reason: expect.stringContaining("docs was not composed") });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("checks the protocol against a pipeline's receivers", () => {
|
|
390
|
+
expect(rowFor({ member: "ops", output: "traces", protocol: "http/protobuf" }, targets([collector]))).toMatchObject({ status: "resolved", protocol: "http/protobuf" });
|
|
391
|
+
const bad = rowFor({ member: "ops", output: "traces", protocol: "grpc" }, targets([collector]));
|
|
392
|
+
expect(bad).toMatchObject({ status: "invalid", target: "pipeline" });
|
|
393
|
+
expect(bad.reason).toBe("the receivers of pipeline traces speak http/protobuf, http/json, and the link sends grpc");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("checks the protocol against an exporter's own", () => {
|
|
397
|
+
expect(rowFor({ member: "ops", output: "otlphttp/backend", protocol: "http/protobuf" }, targets([collector])).status).toBe("resolved");
|
|
398
|
+
expect(rowFor({ member: "ops", output: "otlphttp/backend", protocol: "grpc" }, targets([collector])).status).toBe("invalid");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("leaves the link unresolved when the target's definitions state no protocols", () => {
|
|
402
|
+
const row = rowFor({ member: "ops", output: "metrics", protocol: "grpc" }, targets([collector]));
|
|
403
|
+
expect(row).toMatchObject({ status: "unresolved", reason: "the receivers of pipeline metrics state no protocols, so grpc could not be confirmed" });
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("a link with no protocol checks presence only", () => {
|
|
407
|
+
expect(rowFor({ member: "ops", output: "metrics" }, targets([collector])).status).toBe("resolved");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("without collectors, as in chant workspace check, a telemetry link is kept unresolved", () => {
|
|
411
|
+
const row = rowFor({ member: "ops", output: "traces" });
|
|
412
|
+
expect(row).toMatchObject({ status: "unresolved", reason: expect.stringContaining("read by chant workspace graph") });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("a protocol on a link that isn't a telemetry link is invalid", () => {
|
|
416
|
+
const d = parseDeclaration(
|
|
417
|
+
declaration([{ name: "ops", dir: "ops", kind: "chant" }, { name: "app", dir: "app", kind: "other", because: "x", links: [{ member: "ops", output: "Url", protocol: "grpc" }] }]),
|
|
418
|
+
"chant.workspace.json",
|
|
419
|
+
);
|
|
420
|
+
const row = resolveLinks(d, new Map())[0] as Extract<LinkTableRow, { producer: string }>;
|
|
421
|
+
expect(row).toMatchObject({ status: "invalid", reason: "protocol applies to telemetry links; this link has kind output" });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("graphLinks resolves a telemetry link from the composed collectors, and drops the pointer", () => {
|
|
425
|
+
const rows = graphLinks(decl({ member: "ops", output: "traces", protocol: "http/protobuf" }), { composed: ["ops", "web"], collectors: [collector], exports: [], imports: [] });
|
|
426
|
+
expect(rows).toEqual([
|
|
427
|
+
expect.objectContaining({ consumer: "app", producer: "ops", output: "traces", kind: "telemetry", status: "resolved", protocol: "http/protobuf", target: "pipeline" }),
|
|
428
|
+
]);
|
|
429
|
+
expect("pointer" in rows[0]!).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("the declaration refuses an unknown protocol, and reads a telemetry link's protocol", () => {
|
|
433
|
+
expect(() => decl({ member: "ops", output: "traces", protocol: "carrier-pigeon" })).toThrow(WorkspaceReadError);
|
|
434
|
+
expect(decl({ member: "ops", output: "traces", protocol: "grpc" }).members[3]!.links[0]).toMatchObject({ kind: "telemetry", protocol: "grpc" });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("WSP098 flags a protocol on a link of another kind, and WSP092 accepts telemetry", async () => {
|
|
438
|
+
const root = repo({
|
|
439
|
+
"chant.workspace.json": declaration([
|
|
440
|
+
{ name: "ops", dir: "ops", kind: "chant" },
|
|
441
|
+
{ name: "app", dir: "app", kind: "other", because: "x", links: [{ member: "ops", output: "Url", protocol: "grpc" }, { member: "ops", output: "traces", kind: "telemetry", protocol: "grpc" }] },
|
|
442
|
+
]),
|
|
443
|
+
"ops/chant.config.ts": "export default {};\n",
|
|
444
|
+
"app/README.md": "",
|
|
445
|
+
});
|
|
446
|
+
const report = await runDeclarationChecks(root);
|
|
447
|
+
const ids = report.diagnostics.map((d) => d.ruleId);
|
|
448
|
+
expect(ids).toContain("WSP098");
|
|
449
|
+
expect(ids).not.toContain("WSP092");
|
|
450
|
+
expect(report.diagnostics.find((d) => d.ruleId === "WSP098")!.message).toContain("states protocol grpc");
|
|
451
|
+
expect(report.diagnostics.filter((d) => d.ruleId === "WSP098")).toHaveLength(1);
|
|
452
|
+
// The telemetry link is kept unresolved here: check reads source.
|
|
453
|
+
expect(report.diagnostics.find((d) => d.ruleId === "WSP094")!.message).toContain("read by chant workspace graph");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
338
457
|
describe("the chant repository's own declaration (#2557)", () => {
|
|
339
458
|
test("has no link finding", async () => {
|
|
340
459
|
const report = await runDeclarationChecks(resolve(import.meta.dirname, "../../../.."));
|
package/src/workspace/links.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* is promoted.
|
|
17
17
|
*
|
|
18
18
|
* Every row says how it was found (`declared` or `inferred:joinKey`), where it
|
|
19
|
-
* resolved (`source
|
|
19
|
+
* resolved (`source`, or `live` for the rows of `check --live`, #2549) and its status. A link that
|
|
20
20
|
* can't be resolved is kept, with the reason.
|
|
21
21
|
*
|
|
22
22
|
* {@link resolveLinks} takes the members' handles as plain data, so the same
|
|
@@ -29,11 +29,23 @@
|
|
|
29
29
|
import { joinKey, joinLabel, type JoinLabel } from "../join-key";
|
|
30
30
|
import type { Declaration, LinkDeclaration, Member } from "./declaration";
|
|
31
31
|
import type { KindRegistry, MemberKind } from "./kinds";
|
|
32
|
+
import type { MemberCollector } from "./compose-graph";
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The link kinds chant knows. Closed: a link of any other kind fails closed.
|
|
36
|
+
* `output` reads a named output of the producer. `telemetry` (#2558, D22)
|
|
37
|
+
* sends the consumer's telemetry to a pipeline or exporter of the producer's
|
|
38
|
+
* collector.
|
|
39
|
+
*/
|
|
40
|
+
export const LINK_KINDS = ["output", "telemetry"] as const;
|
|
35
41
|
export const DEFAULT_LINK_KIND = "output";
|
|
36
42
|
|
|
43
|
+
/** The kind a link has when it names a collector target (#2558). */
|
|
44
|
+
export const TELEMETRY_LINK_KIND = "telemetry";
|
|
45
|
+
|
|
46
|
+
/** The OTLP protocols a telemetry link may state, as `OTEL_EXPORTER_OTLP_PROTOCOL` names them. */
|
|
47
|
+
export const TELEMETRY_PROTOCOLS = ["grpc", "http/protobuf", "http/json"] as const;
|
|
48
|
+
|
|
37
49
|
/** How a row was found: written down, or inferred by the named rule. */
|
|
38
50
|
export type LinkOrigin = "declared" | "inferred:joinKey";
|
|
39
51
|
|
|
@@ -65,7 +77,7 @@ interface RowBase {
|
|
|
65
77
|
consumer: string;
|
|
66
78
|
kind: string;
|
|
67
79
|
origin: LinkOrigin;
|
|
68
|
-
resolves: "source";
|
|
80
|
+
resolves: "source" | "live";
|
|
69
81
|
/** Why the row is not `resolved`, or null. */
|
|
70
82
|
reason: string | null;
|
|
71
83
|
}
|
|
@@ -84,6 +96,21 @@ export interface LinkRow extends RowBase {
|
|
|
84
96
|
to?: string;
|
|
85
97
|
/** The declared link's JSON Pointer in the declaration. */
|
|
86
98
|
pointer?: string;
|
|
99
|
+
/** A telemetry link: the OTLP protocol it states (#2558). */
|
|
100
|
+
protocol?: string;
|
|
101
|
+
/** A telemetry link that found its target: what `output` named, a `pipeline` or an `exporter` of the producer's collector. */
|
|
102
|
+
target?: "pipeline" | "exporter";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What a telemetry link reads from the producers (#2558): each composed
|
|
107
|
+
* member's collector, as `chant workspace graph` lists it, and which members
|
|
108
|
+
* were composed. `chant workspace check` reads source and runs no member, so
|
|
109
|
+
* it passes none and a telemetry link stays unresolved there.
|
|
110
|
+
*/
|
|
111
|
+
export interface TelemetryTargets {
|
|
112
|
+
collectors: ReadonlyMap<string, MemberCollector>;
|
|
113
|
+
composed: ReadonlySet<string>;
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
/** A parameter that joins outputs of two or more producers (or two outputs of one). */
|
|
@@ -145,7 +172,7 @@ function nearNames(name: string, outputs: JoinHandle[]): string[] {
|
|
|
145
172
|
* missing from it is unresolved. Declared rows come first, in declaration
|
|
146
173
|
* order; inferred and ambiguous rows follow, sorted by consumer and parameter.
|
|
147
174
|
*/
|
|
148
|
-
export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<string, MemberHandles
|
|
175
|
+
export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<string, MemberHandles>, telemetry?: TelemetryTargets): LinkTableRow[] {
|
|
149
176
|
const rows: LinkTableRow[] = [];
|
|
150
177
|
const declaredTargets = new Map<string, Set<string>>();
|
|
151
178
|
|
|
@@ -164,6 +191,7 @@ export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<stri
|
|
|
164
191
|
status: "resolved",
|
|
165
192
|
reason: null,
|
|
166
193
|
pointer: link.pointer,
|
|
194
|
+
...(link.protocol ? { protocol: link.protocol } : {}),
|
|
167
195
|
};
|
|
168
196
|
rows.push(row);
|
|
169
197
|
const targetProblem = linkTargetProblem(declaration, consumer, link);
|
|
@@ -178,6 +206,15 @@ export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<stri
|
|
|
178
206
|
row.reason = `the link kind ${kind} is not one chant knows; known link kinds: ${LINK_KINDS.join(", ")}`;
|
|
179
207
|
continue;
|
|
180
208
|
}
|
|
209
|
+
if (kind === TELEMETRY_LINK_KIND) {
|
|
210
|
+
resolveTelemetryLink(row, telemetry);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (link.protocol) {
|
|
214
|
+
row.status = "invalid";
|
|
215
|
+
row.reason = `protocol applies to telemetry links; this link has kind ${kind}`;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
181
218
|
const producer = handles.get(link.member);
|
|
182
219
|
const found = producer?.outputs.find((o) => o.name === link.output);
|
|
183
220
|
if (found) {
|
|
@@ -260,6 +297,54 @@ export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<stri
|
|
|
260
297
|
return [...rows, ...inferred];
|
|
261
298
|
}
|
|
262
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Resolve a telemetry link against the producer's collector (#2558): the
|
|
302
|
+
* target exists (presence), and when the link states a protocol, the target
|
|
303
|
+
* speaks it. A pipeline speaks what its receivers speak; an exporter speaks
|
|
304
|
+
* what its definition says. A target whose protocols the collector's
|
|
305
|
+
* definitions don't state leaves the link unresolved rather than failed.
|
|
306
|
+
*/
|
|
307
|
+
function resolveTelemetryLink(row: LinkRow, telemetry: TelemetryTargets | undefined): void {
|
|
308
|
+
if (!telemetry) {
|
|
309
|
+
row.status = "unresolved";
|
|
310
|
+
row.reason = `the collector of ${row.producer} is read by chant workspace graph; chant workspace check reads source and runs no member`;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const collector = telemetry.collectors.get(row.producer);
|
|
314
|
+
if (!collector) {
|
|
315
|
+
if (!telemetry.composed.has(row.producer)) {
|
|
316
|
+
row.status = "unresolved";
|
|
317
|
+
row.reason = `the collector of ${row.producer} was not read: ${row.producer} was not composed`;
|
|
318
|
+
} else {
|
|
319
|
+
row.status = "missing";
|
|
320
|
+
row.reason = `${row.producer} declares no collector: its build reports no collector pipelines or components`;
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const pipeline = collector.pipelines.find((p) => p.id === row.output);
|
|
325
|
+
const exporter = pipeline ? undefined : collector.exporters.find((e) => e.id === row.output);
|
|
326
|
+
if (!pipeline && !exporter) {
|
|
327
|
+
row.status = "missing";
|
|
328
|
+
row.reason =
|
|
329
|
+
`${row.producer}'s collector has no pipeline or exporter ${row.output}` +
|
|
330
|
+
`; pipelines: ${collector.pipelines.map((p) => p.id).join(", ") || "none"}` +
|
|
331
|
+
`; exporters: ${collector.exporters.map((e) => e.id).join(", ") || "none"}`;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
row.target = pipeline ? "pipeline" : "exporter";
|
|
335
|
+
if (!row.protocol) return;
|
|
336
|
+
const ids = pipeline ? pipeline.receivers : [exporter!.id];
|
|
337
|
+
const speaks = [...new Set(collector.components.filter((c) => ids.includes(c.id)).flatMap((c) => c.protocols))];
|
|
338
|
+
const what = pipeline ? `the receivers of pipeline ${row.output}` : `exporter ${row.output}`;
|
|
339
|
+
if (speaks.length === 0) {
|
|
340
|
+
row.status = "unresolved";
|
|
341
|
+
row.reason = `${what} state no protocols, so ${row.protocol} could not be confirmed`;
|
|
342
|
+
} else if (!speaks.includes(row.protocol)) {
|
|
343
|
+
row.status = "invalid";
|
|
344
|
+
row.reason = `${what} speak ${speaks.join(", ")}, and the link sends ${row.protocol}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
263
348
|
// ── Where the handles come from ──────────────────────────────────────────────
|
|
264
349
|
|
|
265
350
|
/**
|
|
@@ -282,6 +367,8 @@ export function kindHandles(m: Member, kind: MemberKind): MemberHandles | undefi
|
|
|
282
367
|
export interface ComposedHandles {
|
|
283
368
|
/** Members whose IR was composed. */
|
|
284
369
|
composed: readonly string[];
|
|
370
|
+
/** The collectors those members report (#2558); a telemetry link resolves against them. */
|
|
371
|
+
collectors?: readonly MemberCollector[];
|
|
285
372
|
exports: readonly { member: string; name: string; node?: string }[];
|
|
286
373
|
imports: readonly { member: string; name: string; node: string }[];
|
|
287
374
|
}
|
|
@@ -322,7 +409,11 @@ export function graphLinks(declaration: Declaration, graph: ComposedHandles, kin
|
|
|
322
409
|
const listed = kindHandles(m, kind);
|
|
323
410
|
if (listed) handles.set(m.name, listed);
|
|
324
411
|
}
|
|
325
|
-
|
|
412
|
+
const telemetry: TelemetryTargets = {
|
|
413
|
+
collectors: new Map((graph.collectors ?? []).map((c) => [c.member, c])),
|
|
414
|
+
composed: new Set(graph.composed),
|
|
415
|
+
};
|
|
416
|
+
return resolveLinks(declaration, handles, telemetry).map((row) => {
|
|
326
417
|
if (row.status === "ambiguous" || row.origin !== "declared") return row;
|
|
327
418
|
// The declaration's pointer means nothing to a graph reader.
|
|
328
419
|
const { pointer: _pointer, ...rest } = row;
|