@chrisdudek/yg 4.1.0 → 4.2.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/bin.js +788 -174
- package/graph-schemas/yg-architecture.yaml +7 -22
- package/graph-schemas/yg-aspect.yaml +39 -4
- package/graph-schemas/yg-flow.yaml +4 -3
- package/graph-schemas/yg-node.yaml +10 -4
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -125,6 +125,7 @@ import { Command } from "commander";
|
|
|
125
125
|
|
|
126
126
|
// src/cli/init.ts
|
|
127
127
|
import chalk from "chalk";
|
|
128
|
+
import { existsSync } from "fs";
|
|
128
129
|
import { mkdir as mkdir2, writeFile as writeFile4, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
129
130
|
import path5 from "path";
|
|
130
131
|
import { fileURLToPath } from "url";
|
|
@@ -191,7 +192,7 @@ The CLI (\`yg\`) reads and validates \u2014 it never modifies files. You create
|
|
|
191
192
|
|
|
192
193
|
**Architecture** \u2014 \`yg-architecture.yaml\` defines the vocabulary: node types, default aspects per type, allowed parent types, allowed relation targets per type. This is the foundation \u2014 read it when starting work on a new repo. Changes require user confirmation. Structure details in \`schemas/yg-architecture.yaml\`.
|
|
193
194
|
|
|
194
|
-
### How Aspects Reach a Node \u2014 7 Channels
|
|
195
|
+
### How Aspects Reach a Node \u2014 7 Channels + Applicability Filter
|
|
195
196
|
|
|
196
197
|
Aspects accumulate from multiple sources simultaneously. The reviewer checks ALL of them \u2014 the node must satisfy every aspect regardless of origin.
|
|
197
198
|
|
|
@@ -221,6 +222,36 @@ Consequences of this cascade:
|
|
|
221
222
|
- Architecture default aspects apply to every node of that type automatically.
|
|
222
223
|
- Implies chains expand recursively. Cycles are forbidden \u2014 CLI detects them.
|
|
223
224
|
|
|
225
|
+
### Applicability Filter (\`when\`) \u2014 Evaluated on Every Channel
|
|
226
|
+
|
|
227
|
+
Seven channels propagate aspects onto nodes. A separate mechanism \u2014 the \`when\`
|
|
228
|
+
predicate \u2014 filters applicability. Every channel's attachment passes through
|
|
229
|
+
\`when\` before the aspect becomes effective. The predicate is evaluated by the
|
|
230
|
+
CLI against the graph (deterministic, no LLM call); if false, the aspect is
|
|
231
|
+
silently skipped on that node.
|
|
232
|
+
|
|
233
|
+
\`when\` can be declared globally on the aspect (applies across channels) or
|
|
234
|
+
per attach site (per channel instance). Global and attach-site \`when\` combine
|
|
235
|
+
via AND on each channel path. Multiple channels deliver independently \u2014 the
|
|
236
|
+
aspect is effective if any channel's path passes both its global and
|
|
237
|
+
attach-site filter.
|
|
238
|
+
|
|
239
|
+
Use \`when\` when an aspect is meaningful for only a subset of nodes under a
|
|
240
|
+
common attach channel. Example: \`external-api-error-mapping\` attached to
|
|
241
|
+
type \`command\` but only applicable when the node calls \`service-client\` \u2014
|
|
242
|
+
declare on the aspect:
|
|
243
|
+
|
|
244
|
+
when:
|
|
245
|
+
relations:
|
|
246
|
+
calls:
|
|
247
|
+
target_type: service-client
|
|
248
|
+
|
|
249
|
+
Domain-neutral examples: a \`pii-encryption\` aspect attached to all
|
|
250
|
+
repositories, but only applicable when the repository stores a field of
|
|
251
|
+
type \`user-profile\`; an \`idempotency-key\` aspect required only for commands
|
|
252
|
+
that emit events; a \`database-migration-review\` aspect only for nodes with
|
|
253
|
+
\`has_mapping: true\` pointing at \`db/migrations/\`.
|
|
254
|
+
|
|
224
255
|
### Reviewer
|
|
225
256
|
|
|
226
257
|
The reviewer is an LLM invoked by \`yg approve\`. It receives: the aspect's content.md + all source files of the node. It checks every rule from content.md against the code.
|
|
@@ -303,6 +334,13 @@ var DECISIONS = `## DECISIONS
|
|
|
303
334
|
|
|
304
335
|
**Architecture change** \u2014 when existing types don't fit the project structure. Always confirm with the user. Never silently modify \`yg-architecture.yaml\`. If a relation between types is forbidden, present the constraint and let the user decide: use an allowed relation type, change the node type, or update the architecture.
|
|
305
336
|
|
|
337
|
+
**\`when\` predicate on an aspect or attach site** \u2014 when the aspect applies to
|
|
338
|
+
only a subset of nodes under a common attach channel. Prefer \`when\` over
|
|
339
|
+
splitting node types (proliferation of types). Prefer \`when\` over leaving
|
|
340
|
+
the decision to the reviewer textually inside \`content.md\`; \`when\` is
|
|
341
|
+
deterministic, has zero LLM cost, and keeps the graph as the source of
|
|
342
|
+
truth for applicability.
|
|
343
|
+
|
|
306
344
|
### Aspect Discovery
|
|
307
345
|
|
|
308
346
|
Aspects emerge from patterns \u2014 greenfield and brownfield:
|
|
@@ -1133,10 +1171,23 @@ var MIGRATIONS = [
|
|
|
1133
1171
|
}
|
|
1134
1172
|
];
|
|
1135
1173
|
|
|
1174
|
+
// src/formatters/message-builder.ts
|
|
1175
|
+
function buildIssueMessage(msg) {
|
|
1176
|
+
return `${msg.what}
|
|
1177
|
+
${msg.why}
|
|
1178
|
+
${msg.next}`;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1136
1181
|
// src/cli/init.ts
|
|
1137
1182
|
function getPackageRoot() {
|
|
1138
|
-
|
|
1139
|
-
|
|
1183
|
+
let dir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1184
|
+
while (dir !== path5.dirname(dir)) {
|
|
1185
|
+
if (existsSync(path5.join(dir, "package.json"))) {
|
|
1186
|
+
return dir;
|
|
1187
|
+
}
|
|
1188
|
+
dir = path5.dirname(dir);
|
|
1189
|
+
}
|
|
1190
|
+
throw new Error("Could not locate package root (no package.json found walking up from init module).");
|
|
1140
1191
|
}
|
|
1141
1192
|
function getGraphSchemasDir() {
|
|
1142
1193
|
return path5.join(getPackageRoot(), "graph-schemas");
|
|
@@ -1438,6 +1489,25 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
|
|
|
1438
1489
|
await writeFile4(path5.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
|
|
1439
1490
|
await installRulesForPlatform(projectRoot, platform);
|
|
1440
1491
|
}
|
|
1492
|
+
async function runVersionUpgrade(projectRoot, yggRoot, fromVersion, toVersion, platform) {
|
|
1493
|
+
const migrationResults = await runMigrations(fromVersion, MIGRATIONS, yggRoot);
|
|
1494
|
+
await updateConfigVersion(yggRoot, toVersion);
|
|
1495
|
+
await refreshSchemas(yggRoot);
|
|
1496
|
+
const architecturePath = path5.join(yggRoot, "yg-architecture.yaml");
|
|
1497
|
+
try {
|
|
1498
|
+
await stat2(architecturePath);
|
|
1499
|
+
} catch {
|
|
1500
|
+
await writeFile4(architecturePath, DEFAULT_ARCHITECTURE, "utf-8");
|
|
1501
|
+
}
|
|
1502
|
+
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1503
|
+
const migrationActions = [];
|
|
1504
|
+
const migrationWarnings = [];
|
|
1505
|
+
for (const r of migrationResults) {
|
|
1506
|
+
migrationActions.push(...r.actions);
|
|
1507
|
+
migrationWarnings.push(...r.warnings);
|
|
1508
|
+
}
|
|
1509
|
+
return { rulesPath, migrationActions, migrationWarnings };
|
|
1510
|
+
}
|
|
1441
1511
|
async function existingInit(projectRoot) {
|
|
1442
1512
|
const yggRoot = path5.join(projectRoot, ".yggdrasil");
|
|
1443
1513
|
if (!isTTY()) {
|
|
@@ -1448,33 +1518,28 @@ async function existingInit(projectRoot) {
|
|
|
1448
1518
|
const currentVersion = await detectVersion(yggRoot);
|
|
1449
1519
|
const cliVersion = await getCliVersion();
|
|
1450
1520
|
if (currentVersion && currentVersion !== cliVersion) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
p.log.info("2. Run yg check to verify graph integrity");
|
|
1474
|
-
p.log.info("3. Run yg approve on all nodes to establish baselines");
|
|
1475
|
-
p.outro(chalk.green(`Migrated from ${currentVersion} to ${cliVersion}.`));
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1521
|
+
p.log.step(`Graph version ${currentVersion} detected \u2014 CLI is ${cliVersion}. Upgrade required.`);
|
|
1522
|
+
p.log.info("Select the agent platform so rules and schemas advance together.");
|
|
1523
|
+
const platform = await promptPlatform();
|
|
1524
|
+
const s = p.spinner();
|
|
1525
|
+
s.start("Running migrations and installing rules...");
|
|
1526
|
+
const result = await runVersionUpgrade(projectRoot, yggRoot, currentVersion, cliVersion, platform);
|
|
1527
|
+
s.stop("Upgrade complete.");
|
|
1528
|
+
for (const action2 of result.migrationActions) {
|
|
1529
|
+
p.log.info(action2);
|
|
1530
|
+
}
|
|
1531
|
+
for (const warning of result.migrationWarnings) {
|
|
1532
|
+
p.log.warning(warning);
|
|
1533
|
+
}
|
|
1534
|
+
p.log.step("Next steps:");
|
|
1535
|
+
p.log.info("1. Run yg check to verify graph integrity");
|
|
1536
|
+
p.log.info("2. Run yg approve on all nodes to establish baselines");
|
|
1537
|
+
p.outro(
|
|
1538
|
+
chalk.green(
|
|
1539
|
+
`Migrated from ${currentVersion} to ${cliVersion}. Rules installed: ${path5.relative(projectRoot, result.rulesPath)}`
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1542
|
+
return;
|
|
1478
1543
|
}
|
|
1479
1544
|
const action = await p.select({
|
|
1480
1545
|
message: "What would you like to do?",
|
|
@@ -1488,15 +1553,9 @@ async function existingInit(projectRoot) {
|
|
|
1488
1553
|
switch (action) {
|
|
1489
1554
|
case "upgrade": {
|
|
1490
1555
|
const platform = await promptPlatform();
|
|
1491
|
-
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
await stat2(architecturePath);
|
|
1495
|
-
} catch {
|
|
1496
|
-
await writeFile4(architecturePath, DEFAULT_ARCHITECTURE, "utf-8");
|
|
1497
|
-
}
|
|
1498
|
-
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1499
|
-
p.outro(chalk.green(`Rules and schemas refreshed: ${path5.relative(projectRoot, rulesPath)}`));
|
|
1556
|
+
const fromVersion = currentVersion ?? cliVersion;
|
|
1557
|
+
const result = await runVersionUpgrade(projectRoot, yggRoot, fromVersion, cliVersion, platform);
|
|
1558
|
+
p.outro(chalk.green(`Rules and schemas refreshed: ${path5.relative(projectRoot, result.rulesPath)}`));
|
|
1500
1559
|
break;
|
|
1501
1560
|
}
|
|
1502
1561
|
case "reviewer": {
|
|
@@ -1537,17 +1596,27 @@ function registerInitCommand(program2) {
|
|
|
1537
1596
|
process.stderr.write(chalk.red("Error: No .yggdrasil/ directory found. Run 'yg init' first.\n"));
|
|
1538
1597
|
process.exit(1);
|
|
1539
1598
|
}
|
|
1540
|
-
await
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1599
|
+
const toVersion = await getCliVersion();
|
|
1600
|
+
const fromVersion = await detectVersion(yggRoot);
|
|
1601
|
+
if (fromVersion === null) {
|
|
1602
|
+
process.stderr.write(chalk.red(buildIssueMessage({
|
|
1603
|
+
what: "No graph version detected.",
|
|
1604
|
+
why: ".yggdrasil/yg-config.yaml is missing a 'version:' field, so --upgrade cannot determine which migrations to run.",
|
|
1605
|
+
next: "Run 'yg init' interactively once to record the current version, then retry 'yg init --upgrade --platform <name>'."
|
|
1606
|
+
}) + "\n"));
|
|
1607
|
+
process.exit(1);
|
|
1546
1608
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1609
|
+
const result = await runVersionUpgrade(
|
|
1610
|
+
projectRoot,
|
|
1611
|
+
yggRoot,
|
|
1612
|
+
fromVersion,
|
|
1613
|
+
toVersion,
|
|
1614
|
+
options.platform
|
|
1615
|
+
);
|
|
1616
|
+
process.stdout.write(
|
|
1617
|
+
`Rules and schemas refreshed: ${path5.relative(projectRoot, result.rulesPath)}
|
|
1618
|
+
`
|
|
1619
|
+
);
|
|
1551
1620
|
return;
|
|
1552
1621
|
}
|
|
1553
1622
|
let exists = false;
|
|
@@ -1704,7 +1773,219 @@ function normalizeProviderConfig(providerName, pc, generalConfig, filename) {
|
|
|
1704
1773
|
// src/io/node-parser.ts
|
|
1705
1774
|
import { readFile as readFile6 } from "fs/promises";
|
|
1706
1775
|
import { parse as parseYaml4 } from "yaml";
|
|
1707
|
-
|
|
1776
|
+
|
|
1777
|
+
// src/io/when-parser.ts
|
|
1778
|
+
var RELATION_TYPES = /* @__PURE__ */ new Set([
|
|
1779
|
+
"calls",
|
|
1780
|
+
"uses",
|
|
1781
|
+
"extends",
|
|
1782
|
+
"implements",
|
|
1783
|
+
"emits",
|
|
1784
|
+
"listens"
|
|
1785
|
+
]);
|
|
1786
|
+
var ATOMIC_KEYS = /* @__PURE__ */ new Set(["relations", "descendants", "node"]);
|
|
1787
|
+
var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["all_of", "any_of", "not"]);
|
|
1788
|
+
function parseWhen(raw, ctx) {
|
|
1789
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1790
|
+
throw new Error(`${ctx}: when must be a YAML mapping`);
|
|
1791
|
+
}
|
|
1792
|
+
const keys = Object.keys(raw);
|
|
1793
|
+
if (keys.length === 0) {
|
|
1794
|
+
throw new Error(`${ctx}: when mapping must not be empty`);
|
|
1795
|
+
}
|
|
1796
|
+
const booleanKeys = keys.filter((k) => BOOLEAN_KEYS.has(k));
|
|
1797
|
+
const atomicKeys = keys.filter((k) => ATOMIC_KEYS.has(k));
|
|
1798
|
+
const unknownKeys = keys.filter((k) => !BOOLEAN_KEYS.has(k) && !ATOMIC_KEYS.has(k));
|
|
1799
|
+
if (unknownKeys.length > 0) {
|
|
1800
|
+
throw new Error(`${ctx}: unknown when operator '${unknownKeys[0]}' (expected one of: all_of, any_of, not, relations, descendants, node)`);
|
|
1801
|
+
}
|
|
1802
|
+
if (booleanKeys.length > 0 && atomicKeys.length > 0) {
|
|
1803
|
+
throw new Error(`${ctx}: when cannot mix boolean operators with atomic clauses at the same level`);
|
|
1804
|
+
}
|
|
1805
|
+
if (booleanKeys.length > 1) {
|
|
1806
|
+
throw new Error(`${ctx}: when can have at most one boolean operator at a level (got: ${booleanKeys.join(", ")})`);
|
|
1807
|
+
}
|
|
1808
|
+
if (booleanKeys.length === 1) {
|
|
1809
|
+
return parseBoolean(raw, booleanKeys[0], ctx);
|
|
1810
|
+
}
|
|
1811
|
+
return parseAtomic(raw, ctx);
|
|
1812
|
+
}
|
|
1813
|
+
function parseBoolean(raw, key, ctx) {
|
|
1814
|
+
const val = raw[key];
|
|
1815
|
+
if (key === "not") {
|
|
1816
|
+
return { not: parseWhen(val, `${ctx}/not`) };
|
|
1817
|
+
}
|
|
1818
|
+
if (!Array.isArray(val)) {
|
|
1819
|
+
throw new Error(`${ctx}: '${key}' must be an array`);
|
|
1820
|
+
}
|
|
1821
|
+
if (val.length === 0) {
|
|
1822
|
+
throw new Error(`${ctx}: '${key}' array must not be empty`);
|
|
1823
|
+
}
|
|
1824
|
+
const items = val.map((v, i) => parseWhen(v, `${ctx}/${key}[${i}]`));
|
|
1825
|
+
return key === "all_of" ? { all_of: items } : { any_of: items };
|
|
1826
|
+
}
|
|
1827
|
+
function parseAtomic(raw, ctx) {
|
|
1828
|
+
const result = {};
|
|
1829
|
+
if ("relations" in raw) {
|
|
1830
|
+
result.relations = parseRelationClause(raw.relations, `${ctx}/relations`);
|
|
1831
|
+
}
|
|
1832
|
+
if ("descendants" in raw) {
|
|
1833
|
+
result.descendants = parseDescendantsClause(raw.descendants, `${ctx}/descendants`);
|
|
1834
|
+
}
|
|
1835
|
+
if ("node" in raw) {
|
|
1836
|
+
result.node = parseNodeClause(raw.node, `${ctx}/node`);
|
|
1837
|
+
}
|
|
1838
|
+
return result;
|
|
1839
|
+
}
|
|
1840
|
+
function parseRelationClause(raw, ctx) {
|
|
1841
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1842
|
+
throw new Error(`${ctx}: relations must be a YAML mapping keyed by relation type`);
|
|
1843
|
+
}
|
|
1844
|
+
const entries = Object.entries(raw);
|
|
1845
|
+
if (entries.length === 0) {
|
|
1846
|
+
throw new Error(`${ctx}: relations mapping must not be empty`);
|
|
1847
|
+
}
|
|
1848
|
+
const out = {};
|
|
1849
|
+
for (const [relType, match] of entries) {
|
|
1850
|
+
if (!RELATION_TYPES.has(relType)) {
|
|
1851
|
+
throw new Error(`${ctx}: unknown relation type '${relType}' (valid: ${Array.from(RELATION_TYPES).join(", ")})`);
|
|
1852
|
+
}
|
|
1853
|
+
out[relType] = parseRelationMatch(match, `${ctx}/${relType}`);
|
|
1854
|
+
}
|
|
1855
|
+
return out;
|
|
1856
|
+
}
|
|
1857
|
+
function parseRelationMatch(raw, ctx) {
|
|
1858
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1859
|
+
throw new Error(`${ctx}: must be a YAML mapping`);
|
|
1860
|
+
}
|
|
1861
|
+
const obj = raw;
|
|
1862
|
+
const allowed = /* @__PURE__ */ new Set(["target_type", "target", "consumes_port"]);
|
|
1863
|
+
for (const k of Object.keys(obj)) {
|
|
1864
|
+
if (!allowed.has(k)) {
|
|
1865
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: target_type, target, consumes_port)`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const out = {};
|
|
1869
|
+
if ("target_type" in obj) {
|
|
1870
|
+
if (typeof obj.target_type !== "string" || obj.target_type.trim() === "") {
|
|
1871
|
+
throw new Error(`${ctx}: target_type must be a non-empty string`);
|
|
1872
|
+
}
|
|
1873
|
+
out.target_type = obj.target_type.trim();
|
|
1874
|
+
}
|
|
1875
|
+
if ("target" in obj) {
|
|
1876
|
+
if (typeof obj.target !== "string" || obj.target.trim() === "") {
|
|
1877
|
+
throw new Error(`${ctx}: target must be a non-empty string (node path relative to model/)`);
|
|
1878
|
+
}
|
|
1879
|
+
out.target = obj.target.trim();
|
|
1880
|
+
}
|
|
1881
|
+
if ("consumes_port" in obj) {
|
|
1882
|
+
if (typeof obj.consumes_port !== "string" || obj.consumes_port.trim() === "") {
|
|
1883
|
+
throw new Error(`${ctx}: consumes_port must be a non-empty string`);
|
|
1884
|
+
}
|
|
1885
|
+
out.consumes_port = obj.consumes_port.trim();
|
|
1886
|
+
}
|
|
1887
|
+
if (Object.keys(out).length === 0) {
|
|
1888
|
+
throw new Error(`${ctx}: at least one of target_type, target, consumes_port must be present`);
|
|
1889
|
+
}
|
|
1890
|
+
return out;
|
|
1891
|
+
}
|
|
1892
|
+
function parseDescendantsClause(raw, ctx) {
|
|
1893
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1894
|
+
throw new Error(`${ctx}: descendants must be a YAML mapping`);
|
|
1895
|
+
}
|
|
1896
|
+
const obj = raw;
|
|
1897
|
+
const allowed = /* @__PURE__ */ new Set(["relations", "type", "has_port"]);
|
|
1898
|
+
for (const k of Object.keys(obj)) {
|
|
1899
|
+
if (!allowed.has(k)) {
|
|
1900
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: relations, type, has_port)`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const out = {};
|
|
1904
|
+
if ("relations" in obj) {
|
|
1905
|
+
out.relations = parseRelationClause(obj.relations, `${ctx}/relations`);
|
|
1906
|
+
}
|
|
1907
|
+
if ("type" in obj) {
|
|
1908
|
+
if (typeof obj.type !== "string" || obj.type.trim() === "") {
|
|
1909
|
+
throw new Error(`${ctx}: type must be a non-empty string`);
|
|
1910
|
+
}
|
|
1911
|
+
out.type = obj.type.trim();
|
|
1912
|
+
}
|
|
1913
|
+
if ("has_port" in obj) {
|
|
1914
|
+
if (typeof obj.has_port !== "string" || obj.has_port.trim() === "") {
|
|
1915
|
+
throw new Error(`${ctx}: has_port must be a non-empty string`);
|
|
1916
|
+
}
|
|
1917
|
+
out.has_port = obj.has_port.trim();
|
|
1918
|
+
}
|
|
1919
|
+
if (Object.keys(out).length === 0) {
|
|
1920
|
+
throw new Error(`${ctx}: at least one of relations, type, has_port must be present`);
|
|
1921
|
+
}
|
|
1922
|
+
return out;
|
|
1923
|
+
}
|
|
1924
|
+
function parseNodeClause(raw, ctx) {
|
|
1925
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1926
|
+
throw new Error(`${ctx}: node must be a YAML mapping`);
|
|
1927
|
+
}
|
|
1928
|
+
const obj = raw;
|
|
1929
|
+
const allowed = /* @__PURE__ */ new Set(["type", "has_port", "has_mapping"]);
|
|
1930
|
+
for (const k of Object.keys(obj)) {
|
|
1931
|
+
if (!allowed.has(k)) {
|
|
1932
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: type, has_port, has_mapping)`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const out = {};
|
|
1936
|
+
if ("type" in obj) {
|
|
1937
|
+
if (typeof obj.type !== "string" || obj.type.trim() === "") {
|
|
1938
|
+
throw new Error(`${ctx}: type must be a non-empty string`);
|
|
1939
|
+
}
|
|
1940
|
+
out.type = obj.type.trim();
|
|
1941
|
+
}
|
|
1942
|
+
if ("has_port" in obj) {
|
|
1943
|
+
if (typeof obj.has_port !== "string" || obj.has_port.trim() === "") {
|
|
1944
|
+
throw new Error(`${ctx}: has_port must be a non-empty string`);
|
|
1945
|
+
}
|
|
1946
|
+
out.has_port = obj.has_port.trim();
|
|
1947
|
+
}
|
|
1948
|
+
if ("has_mapping" in obj) {
|
|
1949
|
+
if (typeof obj.has_mapping !== "boolean") {
|
|
1950
|
+
throw new Error(`${ctx}: has_mapping must be a boolean`);
|
|
1951
|
+
}
|
|
1952
|
+
out.has_mapping = obj.has_mapping;
|
|
1953
|
+
}
|
|
1954
|
+
if (Object.keys(out).length === 0) {
|
|
1955
|
+
throw new Error(`${ctx}: at least one of type, has_port, has_mapping must be present`);
|
|
1956
|
+
}
|
|
1957
|
+
return out;
|
|
1958
|
+
}
|
|
1959
|
+
function parseAspectAttachment(raw, ctx) {
|
|
1960
|
+
if (typeof raw === "string") {
|
|
1961
|
+
const id = raw.trim();
|
|
1962
|
+
if (id === "") {
|
|
1963
|
+
throw new Error(`${ctx}: aspect id must be a non-empty string`);
|
|
1964
|
+
}
|
|
1965
|
+
return { id };
|
|
1966
|
+
}
|
|
1967
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1968
|
+
const obj = raw;
|
|
1969
|
+
if (typeof obj.id !== "string" || obj.id.trim() === "") {
|
|
1970
|
+
throw new Error(`${ctx}: object form requires 'id' as a non-empty string`);
|
|
1971
|
+
}
|
|
1972
|
+
const result = { id: obj.id.trim() };
|
|
1973
|
+
const allowed = /* @__PURE__ */ new Set(["id", "when"]);
|
|
1974
|
+
for (const k of Object.keys(obj)) {
|
|
1975
|
+
if (!allowed.has(k)) {
|
|
1976
|
+
throw new Error(`${ctx}: unknown field '${k}' in aspect attachment (allowed: id, when)`);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if ("when" in obj) {
|
|
1980
|
+
result.when = parseWhen(obj.when, `${ctx}/when`);
|
|
1981
|
+
}
|
|
1982
|
+
return result;
|
|
1983
|
+
}
|
|
1984
|
+
throw new Error(`${ctx}: aspect attachment must be a string or an object with 'id' (and optional 'when')`);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/io/node-parser.ts
|
|
1988
|
+
var RELATION_TYPES2 = [
|
|
1708
1989
|
"uses",
|
|
1709
1990
|
"calls",
|
|
1710
1991
|
"extends",
|
|
@@ -1713,7 +1994,7 @@ var RELATION_TYPES = [
|
|
|
1713
1994
|
"listens"
|
|
1714
1995
|
];
|
|
1715
1996
|
function isValidRelationType(t) {
|
|
1716
|
-
return typeof t === "string" &&
|
|
1997
|
+
return typeof t === "string" && RELATION_TYPES2.includes(t);
|
|
1717
1998
|
}
|
|
1718
1999
|
async function parseNodeYaml(filePath) {
|
|
1719
2000
|
const content = await readFile6(filePath, "utf-8");
|
|
@@ -1730,52 +2011,43 @@ async function parseNodeYaml(filePath) {
|
|
|
1730
2011
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1731
2012
|
const relations = parseRelations(raw.relations, filePath);
|
|
1732
2013
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
1733
|
-
const
|
|
2014
|
+
const aspectsResult = parseAspects(raw.aspects, filePath);
|
|
1734
2015
|
const ports = parsePorts(raw.ports, filePath);
|
|
1735
2016
|
return {
|
|
1736
2017
|
name: raw.name.trim(),
|
|
1737
2018
|
type: raw.type.trim(),
|
|
1738
2019
|
description,
|
|
1739
|
-
aspects,
|
|
2020
|
+
aspects: aspectsResult.aspects,
|
|
2021
|
+
...aspectsResult.aspectWhens && { aspectWhens: aspectsResult.aspectWhens },
|
|
1740
2022
|
relations: relations.length > 0 ? relations : void 0,
|
|
1741
2023
|
mapping,
|
|
1742
2024
|
ports
|
|
1743
2025
|
};
|
|
1744
2026
|
}
|
|
1745
2027
|
function parseAspects(raw, filePath) {
|
|
1746
|
-
if (raw === void 0 || raw === null) return
|
|
2028
|
+
if (raw === void 0 || raw === null) return {};
|
|
1747
2029
|
if (!Array.isArray(raw)) {
|
|
1748
2030
|
throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
|
|
1749
2031
|
}
|
|
1750
|
-
if (raw.length === 0) return
|
|
1751
|
-
const
|
|
1752
|
-
|
|
2032
|
+
if (raw.length === 0) return {};
|
|
2033
|
+
const aspects = [];
|
|
2034
|
+
let aspectWhens;
|
|
2035
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1753
2036
|
for (let i = 0; i < raw.length; i++) {
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
`yg-node.yaml at ${filePath}: aspects[${i}] must be a non-empty string`
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
|
-
} else if (typeof item === "object" && item !== null) {
|
|
1764
|
-
throw new Error(
|
|
1765
|
-
`yg-node.yaml at ${filePath}: aspects must be an array of strings.`
|
|
1766
|
-
);
|
|
1767
|
-
} else {
|
|
1768
|
-
throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be a string`);
|
|
2037
|
+
const parsed = parseAspectAttachment(
|
|
2038
|
+
raw[i],
|
|
2039
|
+
`yg-node.yaml at ${filePath}: aspects[${i}]`
|
|
2040
|
+
);
|
|
2041
|
+
if (seen.has(parsed.id)) {
|
|
2042
|
+
throw new Error(`yg-node.yaml at ${filePath}: duplicate aspect '${parsed.id}' in aspects list`);
|
|
1769
2043
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
);
|
|
2044
|
+
seen.add(parsed.id);
|
|
2045
|
+
aspects.push(parsed.id);
|
|
2046
|
+
if (parsed.when) {
|
|
2047
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
1774
2048
|
}
|
|
1775
|
-
seenAspects.add(aspectId);
|
|
1776
|
-
result.push(aspectId);
|
|
1777
2049
|
}
|
|
1778
|
-
return
|
|
2050
|
+
return { aspects: aspects.length > 0 ? aspects : void 0, aspectWhens };
|
|
1779
2051
|
}
|
|
1780
2052
|
function parseRelations(raw, filePath) {
|
|
1781
2053
|
if (raw === void 0) return [];
|
|
@@ -1867,13 +2139,28 @@ function parsePorts(rawPorts, filePath) {
|
|
|
1867
2139
|
if (!Array.isArray(obj.aspects)) {
|
|
1868
2140
|
throw new Error(`yg-node.yaml at ${filePath}: ports.${name}.aspects must be an array`);
|
|
1869
2141
|
}
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
2142
|
+
const portAspects = [];
|
|
2143
|
+
let portAspectWhens;
|
|
2144
|
+
const seenPortAspects = /* @__PURE__ */ new Set();
|
|
2145
|
+
for (let i = 0; i < obj.aspects.length; i++) {
|
|
2146
|
+
const parsed = parseAspectAttachment(
|
|
2147
|
+
obj.aspects[i],
|
|
2148
|
+
`yg-node.yaml at ${filePath}: ports.${name}.aspects[${i}]`
|
|
2149
|
+
);
|
|
2150
|
+
if (seenPortAspects.has(parsed.id)) {
|
|
2151
|
+
throw new Error(`yg-node.yaml at ${filePath}: ports.${name}.aspects has duplicate '${parsed.id}'`);
|
|
1873
2152
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2153
|
+
seenPortAspects.add(parsed.id);
|
|
2154
|
+
portAspects.push(parsed.id);
|
|
2155
|
+
if (parsed.when) {
|
|
2156
|
+
(portAspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
ports[name] = {
|
|
2160
|
+
description: obj.description.trim(),
|
|
2161
|
+
aspects: portAspects,
|
|
2162
|
+
...portAspectWhens && { aspectWhens: portAspectWhens }
|
|
2163
|
+
};
|
|
1877
2164
|
}
|
|
1878
2165
|
return Object.keys(ports).length > 0 ? ports : void 0;
|
|
1879
2166
|
}
|
|
@@ -1924,17 +2211,34 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1924
2211
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1925
2212
|
const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
|
|
1926
2213
|
let implies;
|
|
2214
|
+
let impliesWhens;
|
|
1927
2215
|
if (raw.implies !== void 0) {
|
|
1928
2216
|
if (!Array.isArray(raw.implies)) {
|
|
1929
|
-
throw new Error(`yg-aspect.yaml at ${aspectYamlPath}: 'implies' must be an array
|
|
2217
|
+
throw new Error(`yg-aspect.yaml at ${aspectYamlPath}: 'implies' must be an array`);
|
|
2218
|
+
}
|
|
2219
|
+
implies = [];
|
|
2220
|
+
for (let i = 0; i < raw.implies.length; i++) {
|
|
2221
|
+
const parsed = parseAspectAttachment(
|
|
2222
|
+
raw.implies[i],
|
|
2223
|
+
`yg-aspect.yaml at ${aspectYamlPath}: implies[${i}]`
|
|
2224
|
+
);
|
|
2225
|
+
implies.push(parsed.id);
|
|
2226
|
+
if (parsed.when) {
|
|
2227
|
+
(impliesWhens ??= {})[parsed.id] = parsed.when;
|
|
2228
|
+
}
|
|
1930
2229
|
}
|
|
1931
|
-
|
|
2230
|
+
}
|
|
2231
|
+
let when;
|
|
2232
|
+
if (raw.when !== void 0) {
|
|
2233
|
+
when = parseWhen(raw.when, `yg-aspect.yaml at ${aspectYamlPath}: when`);
|
|
1932
2234
|
}
|
|
1933
2235
|
return {
|
|
1934
2236
|
name: raw.name.trim(),
|
|
1935
2237
|
id: idTrimmed,
|
|
1936
2238
|
description,
|
|
1937
2239
|
implies,
|
|
2240
|
+
...impliesWhens && { impliesWhens },
|
|
2241
|
+
...when && { when },
|
|
1938
2242
|
artifacts
|
|
1939
2243
|
};
|
|
1940
2244
|
}
|
|
@@ -1966,19 +2270,30 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1966
2270
|
);
|
|
1967
2271
|
}
|
|
1968
2272
|
let aspects;
|
|
2273
|
+
let aspectWhens;
|
|
1969
2274
|
if (raw.aspects !== void 0) {
|
|
1970
2275
|
if (!Array.isArray(raw.aspects)) {
|
|
1971
2276
|
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
1972
2277
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
2278
|
+
aspects = [];
|
|
2279
|
+
for (let i = 0; i < raw.aspects.length; i++) {
|
|
2280
|
+
const parsed = parseAspectAttachment(
|
|
2281
|
+
raw.aspects[i],
|
|
2282
|
+
`yg-flow.yaml at ${flowYamlPath}: aspects[${i}]`
|
|
2283
|
+
);
|
|
2284
|
+
aspects.push(parsed.id);
|
|
2285
|
+
if (parsed.when) {
|
|
2286
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
1975
2289
|
}
|
|
1976
2290
|
return {
|
|
1977
2291
|
path: path8.basename(flowDir),
|
|
1978
2292
|
name: raw.name.trim(),
|
|
1979
2293
|
description,
|
|
1980
2294
|
nodes: nodePaths,
|
|
1981
|
-
...aspects !== void 0 && { aspects }
|
|
2295
|
+
...aspects !== void 0 && { aspects },
|
|
2296
|
+
...aspectWhens && { aspectWhens }
|
|
1982
2297
|
};
|
|
1983
2298
|
}
|
|
1984
2299
|
|
|
@@ -2025,12 +2340,28 @@ async function parseArchitecture(filePath) {
|
|
|
2025
2340
|
`yg-architecture.yaml: node type '${typeName}' has unknown field 'integration_aspects'. Use ports on the target node instead.`
|
|
2026
2341
|
);
|
|
2027
2342
|
}
|
|
2028
|
-
|
|
2343
|
+
let aspects;
|
|
2344
|
+
let aspectWhens;
|
|
2345
|
+
if (Array.isArray(entry.aspects)) {
|
|
2346
|
+
aspects = [];
|
|
2347
|
+
for (let i = 0; i < entry.aspects.length; i++) {
|
|
2348
|
+
const parsed = parseAspectAttachment(
|
|
2349
|
+
entry.aspects[i],
|
|
2350
|
+
`yg-architecture.yaml: node_types.${typeName}.aspects[${i}]`
|
|
2351
|
+
);
|
|
2352
|
+
aspects.push(parsed.id);
|
|
2353
|
+
if (parsed.when) {
|
|
2354
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (aspects.length === 0) aspects = void 0;
|
|
2358
|
+
}
|
|
2029
2359
|
const parents = Array.isArray(entry.parents) ? entry.parents.filter((t) => typeof t === "string") : void 0;
|
|
2030
2360
|
const relations = parseRelations2(entry.relations, typeName);
|
|
2031
2361
|
nodeTypes[typeName] = {
|
|
2032
2362
|
description: entry.description,
|
|
2033
|
-
aspects
|
|
2363
|
+
aspects,
|
|
2364
|
+
...aspectWhens && { aspectWhens },
|
|
2034
2365
|
parents: parents && parents.length > 0 ? parents : void 0,
|
|
2035
2366
|
relations
|
|
2036
2367
|
};
|
|
@@ -2307,57 +2638,191 @@ async function loadSchemas(schemasDir) {
|
|
|
2307
2638
|
import { readFile as readFile13 } from "fs/promises";
|
|
2308
2639
|
import path12 from "path";
|
|
2309
2640
|
|
|
2641
|
+
// src/core/when-evaluator.ts
|
|
2642
|
+
function evaluateWhen(predicate, node, graph) {
|
|
2643
|
+
if ("all_of" in predicate) {
|
|
2644
|
+
return predicate.all_of.every((p2) => evaluateWhen(p2, node, graph));
|
|
2645
|
+
}
|
|
2646
|
+
if ("any_of" in predicate) {
|
|
2647
|
+
return predicate.any_of.some((p2) => evaluateWhen(p2, node, graph));
|
|
2648
|
+
}
|
|
2649
|
+
if ("not" in predicate) {
|
|
2650
|
+
return !evaluateWhen(predicate.not, node, graph);
|
|
2651
|
+
}
|
|
2652
|
+
return evaluateAtomic(predicate, node, graph);
|
|
2653
|
+
}
|
|
2654
|
+
function evaluateAtomic(clause, node, graph) {
|
|
2655
|
+
if (clause.relations) {
|
|
2656
|
+
if (!evaluateRelationClause(clause.relations, node.meta.relations ?? [], graph)) return false;
|
|
2657
|
+
}
|
|
2658
|
+
if (clause.descendants) {
|
|
2659
|
+
if (!evaluateDescendantsClause(clause.descendants, node, graph)) return false;
|
|
2660
|
+
}
|
|
2661
|
+
if (clause.node) {
|
|
2662
|
+
if (!evaluateNodeClause(clause.node, node)) return false;
|
|
2663
|
+
}
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
function evaluateRelationClause(rc, relations, graph) {
|
|
2667
|
+
for (const [relType, match] of Object.entries(rc)) {
|
|
2668
|
+
if (!match) continue;
|
|
2669
|
+
const candidates = relations.filter((r) => r.type === relType);
|
|
2670
|
+
if (!candidates.some((r) => matchesRelation(r, match, graph))) {
|
|
2671
|
+
return false;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
function matchesRelation(r, match, graph) {
|
|
2677
|
+
if (match.target !== void 0 && r.target !== match.target) return false;
|
|
2678
|
+
if (match.target_type !== void 0) {
|
|
2679
|
+
const tgt = graph.nodes.get(r.target);
|
|
2680
|
+
if (!tgt || tgt.meta.type !== match.target_type) return false;
|
|
2681
|
+
}
|
|
2682
|
+
if (match.consumes_port !== void 0) {
|
|
2683
|
+
if (!r.consumes || !r.consumes.includes(match.consumes_port)) return false;
|
|
2684
|
+
}
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
2687
|
+
function evaluateDescendantsClause(dc, node, graph) {
|
|
2688
|
+
const descendants = collectDescendants(node);
|
|
2689
|
+
if (descendants.length === 0) return false;
|
|
2690
|
+
if (dc.type !== void 0) {
|
|
2691
|
+
if (!descendants.some((d) => d.meta.type === dc.type)) return false;
|
|
2692
|
+
}
|
|
2693
|
+
if (dc.has_port !== void 0) {
|
|
2694
|
+
if (!descendants.some((d) => d.meta.ports && Object.prototype.hasOwnProperty.call(d.meta.ports, dc.has_port))) {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (dc.relations) {
|
|
2699
|
+
if (!descendants.some((d) => evaluateRelationClause(dc.relations, d.meta.relations ?? [], graph))) {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return true;
|
|
2704
|
+
}
|
|
2705
|
+
function evaluateNodeClause(nc, node) {
|
|
2706
|
+
if (nc.type !== void 0 && node.meta.type !== nc.type) return false;
|
|
2707
|
+
if (nc.has_port !== void 0) {
|
|
2708
|
+
if (!node.meta.ports || !Object.prototype.hasOwnProperty.call(node.meta.ports, nc.has_port)) return false;
|
|
2709
|
+
}
|
|
2710
|
+
if (nc.has_mapping !== void 0) {
|
|
2711
|
+
const has = (node.meta.mapping?.length ?? 0) > 0;
|
|
2712
|
+
if (has !== nc.has_mapping) return false;
|
|
2713
|
+
}
|
|
2714
|
+
return true;
|
|
2715
|
+
}
|
|
2716
|
+
function collectDescendants(node) {
|
|
2717
|
+
const out = [];
|
|
2718
|
+
const queue = [...node.children];
|
|
2719
|
+
while (queue.length > 0) {
|
|
2720
|
+
const curr = queue.shift();
|
|
2721
|
+
out.push(curr);
|
|
2722
|
+
for (const c of curr.children) queue.push(c);
|
|
2723
|
+
}
|
|
2724
|
+
return out;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2310
2727
|
// src/core/effective-aspects.ts
|
|
2311
2728
|
function computeEffectiveAspects(node, graph) {
|
|
2312
|
-
const
|
|
2729
|
+
const direct = /* @__PURE__ */ new Set();
|
|
2730
|
+
const ancestors = collectAncestors(node);
|
|
2731
|
+
const tryAdd = (aspectId, attachWhen) => {
|
|
2732
|
+
const aspectDef = graph.aspects.find((a) => a.id === aspectId);
|
|
2733
|
+
const globalWhen = aspectDef?.when;
|
|
2734
|
+
if (globalWhen && !evaluateWhen(globalWhen, node, graph)) {
|
|
2735
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${aspectId}' filtered: global when=false`);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (attachWhen && !evaluateWhen(attachWhen, node, graph)) {
|
|
2739
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${aspectId}' filtered: attach-site when=false`);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
direct.add(aspectId);
|
|
2743
|
+
};
|
|
2313
2744
|
for (const id of node.meta.aspects ?? []) {
|
|
2314
|
-
|
|
2745
|
+
tryAdd(id, node.meta.aspectWhens?.[id]);
|
|
2315
2746
|
}
|
|
2316
|
-
const ancestors = collectAncestors(node);
|
|
2317
2747
|
for (const ancestor of ancestors) {
|
|
2318
2748
|
for (const id of ancestor.meta.aspects ?? []) {
|
|
2319
|
-
|
|
2749
|
+
tryAdd(id, ancestor.meta.aspectWhens?.[id]);
|
|
2320
2750
|
}
|
|
2321
2751
|
}
|
|
2322
2752
|
if (graph.architecture) {
|
|
2323
2753
|
const typeDef = graph.architecture.node_types[node.meta.type];
|
|
2324
2754
|
for (const id of typeDef?.aspects ?? []) {
|
|
2325
|
-
|
|
2755
|
+
tryAdd(id, typeDef?.aspectWhens?.[id]);
|
|
2326
2756
|
}
|
|
2327
2757
|
}
|
|
2328
2758
|
if (graph.architecture) {
|
|
2329
2759
|
for (const ancestor of ancestors) {
|
|
2330
2760
|
const typeDef = graph.architecture.node_types[ancestor.meta.type];
|
|
2331
2761
|
for (const id of typeDef?.aspects ?? []) {
|
|
2332
|
-
|
|
2762
|
+
tryAdd(id, typeDef?.aspectWhens?.[id]);
|
|
2333
2763
|
}
|
|
2334
2764
|
}
|
|
2335
2765
|
}
|
|
2336
2766
|
const allPaths = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2337
2767
|
for (const flow of graph.flows) {
|
|
2338
|
-
if (flow.nodes.some((n) => allPaths.has(n)))
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
}
|
|
2768
|
+
if (!flow.nodes.some((n) => allPaths.has(n))) continue;
|
|
2769
|
+
for (const id of flow.aspects ?? []) {
|
|
2770
|
+
tryAdd(id, flow.aspectWhens?.[id]);
|
|
2342
2771
|
}
|
|
2343
2772
|
}
|
|
2344
2773
|
if (node.meta.relations) {
|
|
2345
2774
|
for (const relation of node.meta.relations) {
|
|
2346
2775
|
const targetNode = graph.nodes.get(relation.target);
|
|
2347
2776
|
if (!targetNode) continue;
|
|
2348
|
-
if (relation.consumes
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2777
|
+
if (!relation.consumes || !targetNode.meta.ports) continue;
|
|
2778
|
+
for (const portName of relation.consumes) {
|
|
2779
|
+
const port = targetNode.meta.ports[portName];
|
|
2780
|
+
if (!port?.aspects) continue;
|
|
2781
|
+
for (const id of port.aspects) {
|
|
2782
|
+
tryAdd(id, port.aspectWhens?.[id]);
|
|
2356
2783
|
}
|
|
2357
2784
|
}
|
|
2358
2785
|
}
|
|
2359
2786
|
}
|
|
2360
|
-
return
|
|
2787
|
+
return expandImpliesFiltered(direct, node, graph);
|
|
2788
|
+
}
|
|
2789
|
+
function expandImpliesFiltered(directIds, node, graph) {
|
|
2790
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
2791
|
+
for (const a of graph.aspects) idToAspect.set(a.id, a);
|
|
2792
|
+
const result = /* @__PURE__ */ new Set();
|
|
2793
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2794
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2795
|
+
const visit = (id, implierId) => {
|
|
2796
|
+
if (stack.has(id)) {
|
|
2797
|
+
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
2798
|
+
}
|
|
2799
|
+
if (visited.has(id)) return;
|
|
2800
|
+
const aspectDef = idToAspect.get(id);
|
|
2801
|
+
if (aspectDef?.when && !evaluateWhen(aspectDef.when, node, graph)) {
|
|
2802
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${id}' filtered: global when=false (implies path)`);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
if (implierId) {
|
|
2806
|
+
const implierDef = idToAspect.get(implierId);
|
|
2807
|
+
const perImplies = implierDef?.impliesWhens?.[id];
|
|
2808
|
+
if (perImplies && !evaluateWhen(perImplies, node, graph)) {
|
|
2809
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${id}' filtered: impliesWhens from '${implierId}' is false`);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
stack.add(id);
|
|
2814
|
+
visited.add(id);
|
|
2815
|
+
result.add(id);
|
|
2816
|
+
const implies = aspectDef?.implies;
|
|
2817
|
+
if (implies) {
|
|
2818
|
+
for (const implied of implies) {
|
|
2819
|
+
visit(implied, id);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
stack.delete(id);
|
|
2823
|
+
};
|
|
2824
|
+
for (const id of directIds) visit(id, null);
|
|
2825
|
+
return result;
|
|
2361
2826
|
}
|
|
2362
2827
|
function getAspectSource(aspectId, node, graph) {
|
|
2363
2828
|
if (node.meta.aspects?.includes(aspectId)) {
|
|
@@ -2424,37 +2889,6 @@ function collectAncestors(node) {
|
|
|
2424
2889
|
}
|
|
2425
2890
|
return ancestors;
|
|
2426
2891
|
}
|
|
2427
|
-
function expandImplies(aspectIds, graph) {
|
|
2428
|
-
const idToImplies = /* @__PURE__ */ new Map();
|
|
2429
|
-
for (const aspect of graph.aspects) {
|
|
2430
|
-
if (aspect.implies) {
|
|
2431
|
-
idToImplies.set(aspect.id, aspect.implies);
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
const result = /* @__PURE__ */ new Set();
|
|
2435
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2436
|
-
const stack = /* @__PURE__ */ new Set();
|
|
2437
|
-
function collect(id) {
|
|
2438
|
-
if (stack.has(id)) {
|
|
2439
|
-
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
2440
|
-
}
|
|
2441
|
-
if (visited.has(id)) return;
|
|
2442
|
-
stack.add(id);
|
|
2443
|
-
visited.add(id);
|
|
2444
|
-
result.add(id);
|
|
2445
|
-
const implies = idToImplies.get(id);
|
|
2446
|
-
if (implies) {
|
|
2447
|
-
for (const implied of implies) {
|
|
2448
|
-
collect(implied);
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
stack.delete(id);
|
|
2452
|
-
}
|
|
2453
|
-
for (const id of aspectIds) {
|
|
2454
|
-
collect(id);
|
|
2455
|
-
}
|
|
2456
|
-
return result;
|
|
2457
|
-
}
|
|
2458
2892
|
|
|
2459
2893
|
// src/core/context-builder.ts
|
|
2460
2894
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
@@ -2847,13 +3281,6 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
|
2847
3281
|
return result;
|
|
2848
3282
|
}
|
|
2849
3283
|
|
|
2850
|
-
// src/formatters/message-builder.ts
|
|
2851
|
-
function buildIssueMessage(msg) {
|
|
2852
|
-
return `${msg.what}
|
|
2853
|
-
${msg.why}
|
|
2854
|
-
${msg.next}`;
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
3284
|
// src/core/validator.ts
|
|
2858
3285
|
async function validate(graph, scope = "all") {
|
|
2859
3286
|
const issues = [];
|
|
@@ -2904,6 +3331,7 @@ async function validate(graph, scope = "all") {
|
|
|
2904
3331
|
issues.push(...checkPortAspectsDefined(graph));
|
|
2905
3332
|
issues.push(...checkPortConsumes(graph));
|
|
2906
3333
|
issues.push(...checkOrphanedAspects(graph));
|
|
3334
|
+
issues.push(...checkWhenReferences(graph));
|
|
2907
3335
|
let filtered = issues;
|
|
2908
3336
|
let nodesScanned = graph.nodes.size;
|
|
2909
3337
|
const normalizedScope = scope.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -3721,6 +4149,141 @@ function checkOrphanedAspects(graph) {
|
|
|
3721
4149
|
}
|
|
3722
4150
|
return issues;
|
|
3723
4151
|
}
|
|
4152
|
+
function checkWhenReferences(graph) {
|
|
4153
|
+
const issues = [];
|
|
4154
|
+
const knownTypes = new Set(Object.keys(graph.architecture?.node_types ?? {}));
|
|
4155
|
+
const visitPredicate = (p2, ctx) => {
|
|
4156
|
+
if ("all_of" in p2) {
|
|
4157
|
+
p2.all_of.forEach((c, i) => visitPredicate(c, `${ctx}/all_of[${i}]`));
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
if ("any_of" in p2) {
|
|
4161
|
+
p2.any_of.forEach((c, i) => visitPredicate(c, `${ctx}/any_of[${i}]`));
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if ("not" in p2) {
|
|
4165
|
+
visitPredicate(p2.not, `${ctx}/not`);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
visitAtomic(p2, ctx);
|
|
4169
|
+
};
|
|
4170
|
+
const visitAtomic = (a, ctx) => {
|
|
4171
|
+
if (a.relations) visitRelationClause(a.relations, `${ctx}/relations`);
|
|
4172
|
+
if (a.descendants) visitDescendantsClause(a.descendants, `${ctx}/descendants`);
|
|
4173
|
+
if (a.node) visitNodeClause(a.node, `${ctx}/node`);
|
|
4174
|
+
};
|
|
4175
|
+
const visitRelationClause = (rc, ctx) => {
|
|
4176
|
+
for (const [relType, match] of Object.entries(rc)) {
|
|
4177
|
+
if (!match) continue;
|
|
4178
|
+
if (match.target_type !== void 0 && !knownTypes.has(match.target_type)) {
|
|
4179
|
+
issues.push({
|
|
4180
|
+
severity: "error",
|
|
4181
|
+
code: "when-unknown-type",
|
|
4182
|
+
rule: "when-unknown-type",
|
|
4183
|
+
message: buildIssueMessage({
|
|
4184
|
+
what: `Unknown node type '${match.target_type}' in when at ${ctx}/${relType}.target_type.`,
|
|
4185
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml; it will never evaluate.",
|
|
4186
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4187
|
+
})
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
if (match.target !== void 0 && !graph.nodes.has(match.target)) {
|
|
4191
|
+
issues.push({
|
|
4192
|
+
severity: "error",
|
|
4193
|
+
code: "when-unknown-node",
|
|
4194
|
+
rule: "when-unknown-node",
|
|
4195
|
+
message: buildIssueMessage({
|
|
4196
|
+
what: `Referenced node '${match.target}' in when at ${ctx}/${relType}.target does not exist.`,
|
|
4197
|
+
why: "The predicate targets a node that is not in the graph.",
|
|
4198
|
+
next: `Fix the node path or add the node under .yggdrasil/model/.`
|
|
4199
|
+
})
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
if (match.consumes_port !== void 0 && match.target !== void 0) {
|
|
4203
|
+
const tgt = graph.nodes.get(match.target);
|
|
4204
|
+
if (tgt && !(tgt.meta.ports && match.consumes_port in tgt.meta.ports)) {
|
|
4205
|
+
issues.push({
|
|
4206
|
+
severity: "error",
|
|
4207
|
+
code: "when-unknown-port",
|
|
4208
|
+
rule: "when-unknown-port",
|
|
4209
|
+
message: buildIssueMessage({
|
|
4210
|
+
what: `Port '${match.consumes_port}' is not declared on node '${match.target}' in when at ${ctx}/${relType}.consumes_port.`,
|
|
4211
|
+
why: "The predicate references a port that does not exist on the target node.",
|
|
4212
|
+
next: `Fix the port name or add it to .yggdrasil/model/${match.target}/yg-node.yaml.`
|
|
4213
|
+
})
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
const visitDescendantsClause = (dc, ctx) => {
|
|
4220
|
+
if (dc.relations) visitRelationClause(dc.relations, `${ctx}/relations`);
|
|
4221
|
+
if (dc.type !== void 0 && !knownTypes.has(dc.type)) {
|
|
4222
|
+
issues.push({
|
|
4223
|
+
severity: "error",
|
|
4224
|
+
code: "when-unknown-type",
|
|
4225
|
+
rule: "when-unknown-type",
|
|
4226
|
+
message: buildIssueMessage({
|
|
4227
|
+
what: `Unknown node type '${dc.type}' in when at ${ctx}/type.`,
|
|
4228
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml.",
|
|
4229
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4230
|
+
})
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
const visitNodeClause = (nc, ctx) => {
|
|
4235
|
+
if (nc.type !== void 0 && !knownTypes.has(nc.type)) {
|
|
4236
|
+
issues.push({
|
|
4237
|
+
severity: "error",
|
|
4238
|
+
code: "when-unknown-type",
|
|
4239
|
+
rule: "when-unknown-type",
|
|
4240
|
+
message: buildIssueMessage({
|
|
4241
|
+
what: `Unknown node type '${nc.type}' in when at ${ctx}/type.`,
|
|
4242
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml.",
|
|
4243
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4244
|
+
})
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
};
|
|
4248
|
+
for (const aspect of graph.aspects) {
|
|
4249
|
+
if (aspect.when) visitPredicate(aspect.when, `aspect '${aspect.id}' when`);
|
|
4250
|
+
if (aspect.impliesWhens) {
|
|
4251
|
+
for (const [targetId, pred] of Object.entries(aspect.impliesWhens)) {
|
|
4252
|
+
visitPredicate(pred, `aspect '${aspect.id}' implies[${targetId}] when`);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
if (graph.architecture) {
|
|
4257
|
+
for (const [typeName, typeDef] of Object.entries(graph.architecture.node_types)) {
|
|
4258
|
+
if (!typeDef.aspectWhens) continue;
|
|
4259
|
+
for (const [aspectId, pred] of Object.entries(typeDef.aspectWhens)) {
|
|
4260
|
+
visitPredicate(pred, `architecture node_types.${typeName} aspectWhens[${aspectId}]`);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
4265
|
+
if (node.meta.aspectWhens) {
|
|
4266
|
+
for (const [aspectId, pred] of Object.entries(node.meta.aspectWhens)) {
|
|
4267
|
+
visitPredicate(pred, `node '${nodePath}' aspectWhens[${aspectId}]`);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
if (node.meta.ports) {
|
|
4271
|
+
for (const [portName, portDef] of Object.entries(node.meta.ports)) {
|
|
4272
|
+
if (!portDef.aspectWhens) continue;
|
|
4273
|
+
for (const [aspectId, pred] of Object.entries(portDef.aspectWhens)) {
|
|
4274
|
+
visitPredicate(pred, `node '${nodePath}' ports.${portName} aspectWhens[${aspectId}]`);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
for (const flow of graph.flows) {
|
|
4280
|
+
if (!flow.aspectWhens) continue;
|
|
4281
|
+
for (const [aspectId, pred] of Object.entries(flow.aspectWhens)) {
|
|
4282
|
+
visitPredicate(pred, `flow '${flow.path}' aspectWhens[${aspectId}]`);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return issues;
|
|
4286
|
+
}
|
|
3724
4287
|
|
|
3725
4288
|
// src/cli/owner.ts
|
|
3726
4289
|
import path15 from "path";
|
|
@@ -4018,12 +4581,14 @@ async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
|
4018
4581
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
4019
4582
|
await writeFile5(filePath, content, "utf-8");
|
|
4020
4583
|
}
|
|
4021
|
-
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
4584
|
+
async function garbageCollectDriftState(yggRoot, validNodePaths, shouldKeep) {
|
|
4022
4585
|
const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR);
|
|
4023
4586
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
4024
4587
|
const removed = [];
|
|
4025
4588
|
for (const nodePath of allNodePaths) {
|
|
4026
|
-
|
|
4589
|
+
const inGraph = validNodePaths.has(nodePath);
|
|
4590
|
+
const keep = inGraph && (shouldKeep ? shouldKeep(nodePath) : true);
|
|
4591
|
+
if (!keep) {
|
|
4027
4592
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
4028
4593
|
await rm2(filePath);
|
|
4029
4594
|
await removeEmptyParents(filePath, driftDir);
|
|
@@ -4302,7 +4867,16 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
4302
4867
|
}
|
|
4303
4868
|
async function runGC(graph) {
|
|
4304
4869
|
const validPaths = new Set(graph.nodes.keys());
|
|
4305
|
-
return garbageCollectDriftState(
|
|
4870
|
+
return garbageCollectDriftState(
|
|
4871
|
+
graph.rootPath,
|
|
4872
|
+
validPaths,
|
|
4873
|
+
(nodePath) => {
|
|
4874
|
+
const node = graph.nodes.get(nodePath);
|
|
4875
|
+
if (!node) return false;
|
|
4876
|
+
const effective = computeEffectiveAspects(node, graph);
|
|
4877
|
+
return effective.size > 0;
|
|
4878
|
+
}
|
|
4879
|
+
);
|
|
4306
4880
|
}
|
|
4307
4881
|
async function commitApproval(yggRoot, result) {
|
|
4308
4882
|
if (result.pendingDriftState) {
|
|
@@ -4602,6 +5176,15 @@ async function runCheck(graph, gitTrackedFiles) {
|
|
|
4602
5176
|
coverageIssue = buildCoverageIssue(uncovered, totalFiles);
|
|
4603
5177
|
}
|
|
4604
5178
|
const orphanedPaths = await detectOrphanedDriftState(graph);
|
|
5179
|
+
await garbageCollectDriftState(
|
|
5180
|
+
graph.rootPath,
|
|
5181
|
+
new Set(graph.nodes.keys()),
|
|
5182
|
+
(nodePath) => {
|
|
5183
|
+
const node = graph.nodes.get(nodePath);
|
|
5184
|
+
if (!node) return false;
|
|
5185
|
+
return computeEffectiveAspects(node, graph).size > 0;
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
4605
5188
|
const yggRelative = path19.relative(path19.dirname(graph.rootPath), graph.rootPath).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4606
5189
|
const orphanWarnings = orphanedPaths.map((p2) => ({
|
|
4607
5190
|
severity: "warning",
|
|
@@ -5250,26 +5833,57 @@ async function loadSecrets(rootPath, providerName) {
|
|
|
5250
5833
|
return void 0;
|
|
5251
5834
|
}
|
|
5252
5835
|
const raw = parseYaml9(content);
|
|
5253
|
-
if (
|
|
5254
|
-
if (
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
}
|
|
5262
|
-
return void 0;
|
|
5263
|
-
|
|
5264
|
-
|
|
5836
|
+
if (raw === null || raw === void 0) return void 0;
|
|
5837
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
5838
|
+
throw new Error(`yg-secrets.yaml: top level must be a YAML mapping`);
|
|
5839
|
+
}
|
|
5840
|
+
const rawObj = raw;
|
|
5841
|
+
if (rawObj.reviewer === void 0) return void 0;
|
|
5842
|
+
if (typeof rawObj.reviewer !== "object" || rawObj.reviewer === null || Array.isArray(rawObj.reviewer)) {
|
|
5843
|
+
throw new Error(`yg-secrets.yaml: 'reviewer' must be a YAML mapping`);
|
|
5844
|
+
}
|
|
5845
|
+
if (!providerName) return void 0;
|
|
5846
|
+
const reviewerRaw = rawObj.reviewer;
|
|
5847
|
+
const providerSection = reviewerRaw[providerName];
|
|
5848
|
+
if (providerSection === void 0) return void 0;
|
|
5849
|
+
if (typeof providerSection !== "object" || providerSection === null || Array.isArray(providerSection)) {
|
|
5850
|
+
throw new Error(`yg-secrets.yaml: 'reviewer.${providerName}' must be a YAML mapping`);
|
|
5851
|
+
}
|
|
5852
|
+
return extractSecretFields(providerSection, providerName);
|
|
5853
|
+
}
|
|
5854
|
+
function extractSecretFields(raw, providerName) {
|
|
5855
|
+
const ctx = (field) => `yg-secrets.yaml at reviewer.${providerName}.${field}`;
|
|
5265
5856
|
const partial = {};
|
|
5266
|
-
if (
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
if (
|
|
5271
|
-
|
|
5272
|
-
|
|
5857
|
+
if (raw.api_key !== void 0) {
|
|
5858
|
+
if (typeof raw.api_key !== "string") throw new Error(`${ctx("api_key")}: must be a string`);
|
|
5859
|
+
partial.api_key = raw.api_key;
|
|
5860
|
+
}
|
|
5861
|
+
if (raw.provider !== void 0) {
|
|
5862
|
+
if (typeof raw.provider !== "string") throw new Error(`${ctx("provider")}: must be a string`);
|
|
5863
|
+
partial.provider = raw.provider;
|
|
5864
|
+
}
|
|
5865
|
+
if (raw.model !== void 0) {
|
|
5866
|
+
if (typeof raw.model !== "string") throw new Error(`${ctx("model")}: must be a string`);
|
|
5867
|
+
partial.model = raw.model;
|
|
5868
|
+
}
|
|
5869
|
+
if (raw.endpoint !== void 0) {
|
|
5870
|
+
if (typeof raw.endpoint !== "string") throw new Error(`${ctx("endpoint")}: must be a string`);
|
|
5871
|
+
partial.endpoint = raw.endpoint;
|
|
5872
|
+
}
|
|
5873
|
+
if (raw.temperature !== void 0) {
|
|
5874
|
+
if (typeof raw.temperature !== "number") throw new Error(`${ctx("temperature")}: must be a number`);
|
|
5875
|
+
partial.temperature = raw.temperature;
|
|
5876
|
+
}
|
|
5877
|
+
if (raw.consensus !== void 0) {
|
|
5878
|
+
if (typeof raw.consensus !== "number") throw new Error(`${ctx("consensus")}: must be a number`);
|
|
5879
|
+
partial.consensus = raw.consensus;
|
|
5880
|
+
}
|
|
5881
|
+
if (raw.max_tokens !== void 0) {
|
|
5882
|
+
if (typeof raw.max_tokens !== "number" && raw.max_tokens !== "auto") {
|
|
5883
|
+
throw new Error(`${ctx("max_tokens")}: must be a number or 'auto'`);
|
|
5884
|
+
}
|
|
5885
|
+
partial.max_tokens = raw.max_tokens;
|
|
5886
|
+
}
|
|
5273
5887
|
return Object.keys(partial).length > 0 ? partial : void 0;
|
|
5274
5888
|
}
|
|
5275
5889
|
function mergeLlmConfig(base, secrets) {
|
|
@@ -5766,7 +6380,7 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
5766
6380
|
}
|
|
5767
6381
|
return chains.sort();
|
|
5768
6382
|
}
|
|
5769
|
-
function
|
|
6383
|
+
function collectDescendants2(graph, nodePath) {
|
|
5770
6384
|
const node = graph.nodes.get(nodePath);
|
|
5771
6385
|
if (!node) return [];
|
|
5772
6386
|
const result = [];
|
|
@@ -5920,7 +6534,7 @@ async function handleFlowImpact(graph, flowName) {
|
|
|
5920
6534
|
for (const nodePath of flow.nodes) {
|
|
5921
6535
|
if (graph.nodes.has(nodePath)) {
|
|
5922
6536
|
participants.add(nodePath);
|
|
5923
|
-
for (const desc of
|
|
6537
|
+
for (const desc of collectDescendants2(graph, nodePath)) {
|
|
5924
6538
|
participants.add(desc);
|
|
5925
6539
|
}
|
|
5926
6540
|
}
|
|
@@ -6096,7 +6710,7 @@ function registerImpactCommand(program2) {
|
|
|
6096
6710
|
`);
|
|
6097
6711
|
}
|
|
6098
6712
|
}
|
|
6099
|
-
const descendants =
|
|
6713
|
+
const descendants = collectDescendants2(graph, nodePath);
|
|
6100
6714
|
if (descendants.length > 0) {
|
|
6101
6715
|
process.stdout.write("\nDescendants (hierarchy impact):\n");
|
|
6102
6716
|
for (const desc of descendants) {
|