@chrisdudek/yg 5.4.0 → 5.4.2
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 +267 -54
- package/dist/templates/portal/app.css +278 -0
- package/dist/templates/portal/shell.css +257 -0
- package/dist/templates/portal/shell.html +39 -0
- package/dist/templates/portal/tokens.css +269 -0
- package/dist/templates/portal/views-audit.css +108 -0
- package/dist/templates/portal/views-flows.css +258 -0
- package/dist/templates/portal/views-panel.css +249 -0
- package/dist/templates/portal/views-relations.css +218 -0
- package/dist/templates/portal/views-rulebook.css +279 -0
- package/dist/templates/portal/views-start.css +208 -0
- package/dist/templates/portal/views.css +340 -0
- package/package.json +4 -2
package/dist/bin.js
CHANGED
|
@@ -17,6 +17,17 @@ var DEFAULT_CONFIG = `version: "5.1.0"
|
|
|
17
17
|
quality:
|
|
18
18
|
max_direct_relations: 10
|
|
19
19
|
|
|
20
|
+
# Coverage \u2014 which files must belong to a node.
|
|
21
|
+
# Fresh projects start in "require nothing" mode: every unmapped file surfaces as
|
|
22
|
+
# a NON-blocking warning (not a blocking error), so your very first \`yg check\` is
|
|
23
|
+
# green while you adopt incrementally. Add a path prefix to \`required\` (e.g.
|
|
24
|
+
# "src/") to make that area blocking once you start mapping it. NOTE: an ABSENT
|
|
25
|
+
# coverage block defaults to requiring the WHOLE repo \u2014 this explicit empty list
|
|
26
|
+
# is what opts a fresh project into require-nothing.
|
|
27
|
+
coverage:
|
|
28
|
+
required: []
|
|
29
|
+
excluded: []
|
|
30
|
+
|
|
20
31
|
# Reviewer configuration added by: yg init
|
|
21
32
|
# (see yg schemas read config + yg knowledge read configuration)
|
|
22
33
|
|
|
@@ -3890,6 +3901,13 @@ async function ensureYggdrasilGitignore(yggRoot) {
|
|
|
3890
3901
|
}
|
|
3891
3902
|
var API_PROVIDERS = ["anthropic", "openai", "google", "openai-compatible", "ollama"];
|
|
3892
3903
|
var CLI_PROVIDERS = ["claude-code", "codex", "gemini-cli"];
|
|
3904
|
+
var ALL_PROVIDERS = [...CLI_PROVIDERS, ...API_PROVIDERS];
|
|
3905
|
+
var API_KEY_ENV = {
|
|
3906
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
3907
|
+
openai: "OPENAI_API_KEY",
|
|
3908
|
+
google: "GOOGLE_API_KEY",
|
|
3909
|
+
"openai-compatible": "OPENAI_API_KEY"
|
|
3910
|
+
};
|
|
3893
3911
|
var CLAUDE_CODE_ALIASES = [
|
|
3894
3912
|
{ value: "haiku", label: "haiku" },
|
|
3895
3913
|
{ value: "sonnet", label: "sonnet" },
|
|
@@ -3984,15 +4002,16 @@ async function promptModelText(provider) {
|
|
|
3984
4002
|
async function runReviewerConfigFlow() {
|
|
3985
4003
|
const provider = await p.select({
|
|
3986
4004
|
message: "Which provider should verify your code?",
|
|
4005
|
+
initialValue: "claude-code",
|
|
3987
4006
|
options: [
|
|
3988
|
-
{ value: "
|
|
3989
|
-
{ value: "
|
|
3990
|
-
{ value: "
|
|
3991
|
-
{ value: "ollama", label: "Ollama", hint: "Local \u2014 no API costs" },
|
|
3992
|
-
{ value: "
|
|
3993
|
-
{ value: "
|
|
3994
|
-
{ value: "
|
|
3995
|
-
{ value: "
|
|
4007
|
+
{ value: "claude-code", label: "Claude Code", hint: "CLI \u2014 free, no API key; uses installed claude" },
|
|
4008
|
+
{ value: "codex", label: "Codex", hint: "CLI \u2014 free, no API key; uses installed codex" },
|
|
4009
|
+
{ value: "gemini-cli", label: "Gemini CLI", hint: "CLI \u2014 free, no API key; uses installed gemini" },
|
|
4010
|
+
{ value: "ollama", label: "Ollama", hint: "Local \u2014 no API costs; needs a local install" },
|
|
4011
|
+
{ value: "anthropic", label: "Anthropic", hint: "API key \u2014 Claude models" },
|
|
4012
|
+
{ value: "openai", label: "OpenAI", hint: "API key \u2014 GPT models" },
|
|
4013
|
+
{ value: "google", label: "Google", hint: "API key \u2014 Gemini models" },
|
|
4014
|
+
{ value: "openai-compatible", label: "OpenAI-compatible", hint: "API key \u2014 custom endpoint" }
|
|
3996
4015
|
]
|
|
3997
4016
|
});
|
|
3998
4017
|
assertNotCancelled(provider);
|
|
@@ -4123,9 +4142,9 @@ async function freshInit(projectRoot) {
|
|
|
4123
4142
|
const yggRoot = path14.join(projectRoot, ".yggdrasil");
|
|
4124
4143
|
if (!isTTY()) {
|
|
4125
4144
|
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4126
|
-
what: "yg init requires an interactive terminal.",
|
|
4127
|
-
why: "
|
|
4128
|
-
next:
|
|
4145
|
+
what: "yg init requires an interactive terminal (no --provider given).",
|
|
4146
|
+
why: "The interactive wizard needs prompts to configure platform and reviewer. Docker, devcontainer, and CI runs have no TTY.",
|
|
4147
|
+
next: `Run yg init in an interactive terminal, OR bootstrap non-interactively: yg init --platform <name> --provider <name> [--model <m>] [--endpoint <url>]. Supported providers: ${ALL_PROVIDERS.join(", ")}.`
|
|
4129
4148
|
})}
|
|
4130
4149
|
`));
|
|
4131
4150
|
process.exit(1);
|
|
@@ -4139,7 +4158,7 @@ async function freshInit(projectRoot) {
|
|
|
4139
4158
|
const platform = await promptPlatform();
|
|
4140
4159
|
p.log.step("Step 2: Reviewer provider");
|
|
4141
4160
|
p.log.info(
|
|
4142
|
-
"The reviewer checks your source code against aspect rules during yg check --approve.\n
|
|
4161
|
+
"The reviewer checks your source code against aspect rules during yg check --approve.\n If you already run an agent CLI (Claude Code, Codex, Gemini), pick it \u2014 the\n reviewer then needs no API key and adds no separate API bill. Ollama runs\n locally with no API cost. API providers (Anthropic, OpenAI, Google) need a key."
|
|
4143
4162
|
);
|
|
4144
4163
|
const reviewerConfig = await runReviewerConfigFlow();
|
|
4145
4164
|
await createYggdrasilStructure(projectRoot, yggRoot, platform);
|
|
@@ -4159,6 +4178,54 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
|
|
|
4159
4178
|
await ensureYggdrasilGitignore(yggRoot);
|
|
4160
4179
|
await installRulesForPlatform(projectRoot, platform);
|
|
4161
4180
|
}
|
|
4181
|
+
async function freshInitNonInteractive(projectRoot, yggRoot, opts) {
|
|
4182
|
+
const { platform, provider } = opts;
|
|
4183
|
+
const model = opts.model?.trim();
|
|
4184
|
+
if (!model) {
|
|
4185
|
+
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4186
|
+
what: "--model is required for non-interactive init.",
|
|
4187
|
+
why: "Non-interactive init records the reviewer model verbatim and applies no default \u2014 the model must be named explicitly.",
|
|
4188
|
+
next: `Re-run naming a model: yg init --platform ${platform} --provider ${provider} --model <name>.`
|
|
4189
|
+
})}
|
|
4190
|
+
`));
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
}
|
|
4193
|
+
let endpoint = opts.endpoint?.trim() || void 0;
|
|
4194
|
+
if (needsEndpoint(provider) && !endpoint) {
|
|
4195
|
+
if (provider === "ollama") {
|
|
4196
|
+
endpoint = "http://localhost:11434";
|
|
4197
|
+
} else {
|
|
4198
|
+
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4199
|
+
what: `--endpoint is required for provider '${provider}'.`,
|
|
4200
|
+
why: "An OpenAI-compatible provider has no default base URL \u2014 the reviewer needs an endpoint to call.",
|
|
4201
|
+
next: `Re-run naming an endpoint: yg init --platform ${platform} --provider ${provider} --model ${model} --endpoint <url>.`
|
|
4202
|
+
})}
|
|
4203
|
+
`));
|
|
4204
|
+
process.exit(1);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
await createYggdrasilStructure(projectRoot, yggRoot, platform);
|
|
4208
|
+
await writeReviewerConfig(yggRoot, { provider, model, endpoint });
|
|
4209
|
+
if (needsApiKey(provider)) {
|
|
4210
|
+
const envVar = API_KEY_ENV[provider];
|
|
4211
|
+
const apiKey = (envVar ? process.env[envVar] : void 0)?.trim();
|
|
4212
|
+
if (apiKey) {
|
|
4213
|
+
await writeSecretsFile(yggRoot, apiKey);
|
|
4214
|
+
} else {
|
|
4215
|
+
process.stdout.write(chalk2.yellow(`${buildIssueMessage({
|
|
4216
|
+
what: `No API key found${envVar ? ` in $${envVar}` : ""}; wrote the config without one.`,
|
|
4217
|
+
why: "An API provider needs a key before the reviewer can run; init records the config anyway so setup is not blocked.",
|
|
4218
|
+
next: `Set ${envVar ?? "the provider API key environment variable"} (or add the key to .yggdrasil/yg-secrets.yaml) before running yg check --approve.`
|
|
4219
|
+
})}
|
|
4220
|
+
`));
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
await ensureGitattributes(projectRoot);
|
|
4224
|
+
process.stdout.write(chalk2.green(
|
|
4225
|
+
`Yggdrasil initialized (platform: ${platform}, provider: ${provider}, model: ${model}). Run yg check to get started.
|
|
4226
|
+
`
|
|
4227
|
+
));
|
|
4228
|
+
}
|
|
4162
4229
|
async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
|
|
4163
4230
|
const { migrationActions, migrationWarnings, withheld } = await runVersionUpgrade({
|
|
4164
4231
|
yggRoot,
|
|
@@ -4249,7 +4316,7 @@ async function existingInit(projectRoot) {
|
|
|
4249
4316
|
}
|
|
4250
4317
|
}
|
|
4251
4318
|
function registerInitCommand(program2) {
|
|
4252
|
-
program2.command("init").description("Initialize Yggdrasil graph in current project").option("--upgrade", "Non-interactive: refresh rules").option("--platform <name>", `Platform for rules file (${PLATFORMS.join(", ")})`).action(async (options) => {
|
|
4319
|
+
program2.command("init").description("Initialize Yggdrasil graph in current project").option("--upgrade", "Non-interactive: refresh rules").option("--platform <name>", `Platform for rules file (${PLATFORMS.join(", ")})`).option("--provider <name>", `Non-interactive fresh init: reviewer provider (${ALL_PROVIDERS.join(", ")})`).option("--model <name>", "Non-interactive fresh init: reviewer model (required)").option("--endpoint <url>", "Non-interactive fresh init: reviewer endpoint (ollama / openai-compatible)").action(async (options) => {
|
|
4253
4320
|
try {
|
|
4254
4321
|
const projectRoot = process.cwd();
|
|
4255
4322
|
const yggRoot = path14.join(projectRoot, ".yggdrasil");
|
|
@@ -4359,6 +4426,40 @@ function registerInitCommand(program2) {
|
|
|
4359
4426
|
}
|
|
4360
4427
|
if (exists) {
|
|
4361
4428
|
await existingInit(projectRoot);
|
|
4429
|
+
} else if (options.provider) {
|
|
4430
|
+
if (!options.platform) {
|
|
4431
|
+
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4432
|
+
what: "--provider requires --platform for non-interactive init.",
|
|
4433
|
+
why: "A fresh graph must install the rules file for a specific agent platform; there is no prompt to ask in non-interactive mode.",
|
|
4434
|
+
next: `Pass --platform <name>. Supported: ${PLATFORMS.join(", ")}.`
|
|
4435
|
+
})}
|
|
4436
|
+
`));
|
|
4437
|
+
process.exit(1);
|
|
4438
|
+
}
|
|
4439
|
+
if (!PLATFORMS.includes(options.platform)) {
|
|
4440
|
+
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4441
|
+
what: `Unknown platform '${options.platform}'.`,
|
|
4442
|
+
why: "The --platform value must match one of the supported agent platforms.",
|
|
4443
|
+
next: `Use one of: ${PLATFORMS.join(", ")}`
|
|
4444
|
+
})}
|
|
4445
|
+
`));
|
|
4446
|
+
process.exit(1);
|
|
4447
|
+
}
|
|
4448
|
+
if (!ALL_PROVIDERS.includes(options.provider)) {
|
|
4449
|
+
process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
|
|
4450
|
+
what: `Unknown provider '${options.provider}'.`,
|
|
4451
|
+
why: "The --provider value must match one of the supported reviewer providers.",
|
|
4452
|
+
next: `Use one of: ${ALL_PROVIDERS.join(", ")}`
|
|
4453
|
+
})}
|
|
4454
|
+
`));
|
|
4455
|
+
process.exit(1);
|
|
4456
|
+
}
|
|
4457
|
+
await freshInitNonInteractive(projectRoot, yggRoot, {
|
|
4458
|
+
platform: options.platform,
|
|
4459
|
+
provider: options.provider,
|
|
4460
|
+
model: options.model,
|
|
4461
|
+
endpoint: options.endpoint
|
|
4462
|
+
});
|
|
4362
4463
|
} else {
|
|
4363
4464
|
await freshInit(projectRoot);
|
|
4364
4465
|
}
|
|
@@ -16056,6 +16157,7 @@ async function runFill(graph, opts) {
|
|
|
16056
16157
|
}
|
|
16057
16158
|
const detPairs = unverifiedPairs.filter((p2) => p2.kind === "deterministic");
|
|
16058
16159
|
const llmPairs = onlyDeterministic ? [] : unverifiedPairs.filter((p2) => p2.kind === "llm");
|
|
16160
|
+
const skippedLlmPairs = onlyDeterministic ? unverifiedPairs.filter((p2) => p2.kind === "llm").length : 0;
|
|
16059
16161
|
const aspectById = /* @__PURE__ */ new Map();
|
|
16060
16162
|
for (const a of graph.aspects) aspectById.set(a.id, a);
|
|
16061
16163
|
const deterministicAspectIds = new Set(
|
|
@@ -16073,6 +16175,12 @@ async function runFill(graph, opts) {
|
|
|
16073
16175
|
`Filling ${unverifiedPairs.length} unverified pairs across ${nodeSet.size} nodes \u2014 ${detPairs.length} deterministic (no cost), ${reviewerCallBudget} reviewer calls (consensus included)
|
|
16074
16176
|
`
|
|
16075
16177
|
);
|
|
16178
|
+
if (skippedLlmPairs > 0) {
|
|
16179
|
+
write(
|
|
16180
|
+
` Deterministic-only mode \u2014 ${skippedLlmPairs} LLM pair${skippedLlmPairs === 1 ? "" : "s"} will NOT be reviewed this run; run \`yg check --approve\` to review ${skippedLlmPairs === 1 ? "it" : "them"}.
|
|
16181
|
+
`
|
|
16182
|
+
);
|
|
16183
|
+
}
|
|
16076
16184
|
if (dryRun) {
|
|
16077
16185
|
const byNode = /* @__PURE__ */ new Map();
|
|
16078
16186
|
for (const p2 of unverifiedPairs) {
|
|
@@ -16113,7 +16221,7 @@ async function runFill(graph, opts) {
|
|
|
16113
16221
|
return writeChain;
|
|
16114
16222
|
};
|
|
16115
16223
|
const setEntry = async (aspectId, unitKey, entry) => {
|
|
16116
|
-
(lock.verdicts[aspectId] ??= {})[unitKey] = entry;
|
|
16224
|
+
(lock.verdicts[aspectId] ??= {})[toPosixPath(unitKey)] = entry;
|
|
16117
16225
|
await persistLock();
|
|
16118
16226
|
};
|
|
16119
16227
|
const blockedNodes = /* @__PURE__ */ new Set();
|
|
@@ -16274,7 +16382,14 @@ async function runFill(graph, opts) {
|
|
|
16274
16382
|
}
|
|
16275
16383
|
await garbageCollectAndRewrite(graph, lock, persistLock);
|
|
16276
16384
|
if (reviewerCallsMade === 0 && infraFailures === 0 && runtimeErrors === 0 && companionRuntimeErrors === 0) {
|
|
16277
|
-
|
|
16385
|
+
if (skippedLlmPairs > 0) {
|
|
16386
|
+
write(
|
|
16387
|
+
`0 reviewer calls made \u2014 deterministic-only mode; ${skippedLlmPairs} LLM pair${skippedLlmPairs === 1 ? "" : "s"} left unverified. Run \`yg check --approve\` to review ${skippedLlmPairs === 1 ? "it" : "them"}.
|
|
16388
|
+
`
|
|
16389
|
+
);
|
|
16390
|
+
} else {
|
|
16391
|
+
write("0 reviewer calls made \u2014 all expected pairs hold valid verdicts\n");
|
|
16392
|
+
}
|
|
16278
16393
|
}
|
|
16279
16394
|
if (infraFailures > 0) {
|
|
16280
16395
|
const providers = [...new Set(infraReport.map((r) => r.provider).filter(Boolean))].join(", ");
|
|
@@ -16608,6 +16723,10 @@ function resolveTopValue(raw) {
|
|
|
16608
16723
|
if (Number.isNaN(n) || n < 1) return null;
|
|
16609
16724
|
return n;
|
|
16610
16725
|
}
|
|
16726
|
+
function nextPointer(next) {
|
|
16727
|
+
const firstLine = next.split("\n")[0];
|
|
16728
|
+
return firstLine.trimEnd().endsWith(":") ? next : firstLine;
|
|
16729
|
+
}
|
|
16611
16730
|
function residualAfterNext(result) {
|
|
16612
16731
|
if (!result.suggestedNext?.startsWith("yg check --approve")) return "";
|
|
16613
16732
|
const errors = result.issues.filter((i) => i.severity === "error");
|
|
@@ -16656,7 +16775,7 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
|
|
|
16656
16775
|
}
|
|
16657
16776
|
const firstFiltered = [...filteredErrors, ...filteredWarnings][0];
|
|
16658
16777
|
if (firstFiltered?.messageData.next) {
|
|
16659
|
-
const nextCmd = firstFiltered.messageData.next
|
|
16778
|
+
const nextCmd = nextPointer(firstFiltered.messageData.next);
|
|
16660
16779
|
sections.push("");
|
|
16661
16780
|
sections.push(`Next (this group): ${nextCmd}`);
|
|
16662
16781
|
sections.push("");
|
|
@@ -16686,7 +16805,7 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
|
|
|
16686
16805
|
}
|
|
16687
16806
|
}
|
|
16688
16807
|
if (result.suggestedNext) {
|
|
16689
|
-
const nextCmd = result.suggestedNext
|
|
16808
|
+
const nextCmd = nextPointer(result.suggestedNext);
|
|
16690
16809
|
const residual = view.kind === "full" || view.kind === "details" ? residualAfterNext(result) : "";
|
|
16691
16810
|
sections.push("");
|
|
16692
16811
|
sections.push(`Next: ${nextCmd}${residual}`);
|
|
@@ -21045,8 +21164,11 @@ cached and FINAL for unchanged inputs. Interrupting is safe \u2014 finished pair
|
|
|
21045
21164
|
persist, the next run resumes.
|
|
21046
21165
|
|
|
21047
21166
|
When nothing was unverified, the summary says \`0 reviewer calls made \u2014 all
|
|
21048
|
-
expected pairs hold valid verdicts\`.
|
|
21049
|
-
|
|
21167
|
+
expected pairs hold valid verdicts\`. Under \`--only-deterministic\` the header and
|
|
21168
|
+
summary instead name the LLM pairs left unverified \u2014 they are skipped by design,
|
|
21169
|
+
not reviewed \u2014 and point at a full \`yg check --approve\` to review them, so a
|
|
21170
|
+
deterministic-only run never reads as if it verified everything. Use \`yg impact\`
|
|
21171
|
+
to predict cost before editing.
|
|
21050
21172
|
|
|
21051
21173
|
\`--dry-run\` (with \`--approve\`) is a free cost preview: it runs the same
|
|
21052
21174
|
structural gate, pair classification, and budget computation, prints the
|
|
@@ -23135,16 +23257,6 @@ async function computePortalFreshness(graph, lock) {
|
|
|
23135
23257
|
}
|
|
23136
23258
|
|
|
23137
23259
|
// src/portal/derive-node-state.ts
|
|
23138
|
-
var PAIR_RANK = {
|
|
23139
|
-
refused: 4,
|
|
23140
|
-
unverified: 3,
|
|
23141
|
-
warning: 2,
|
|
23142
|
-
verified: 1,
|
|
23143
|
-
"n/a": 0
|
|
23144
|
-
};
|
|
23145
|
-
function worstPairState(states) {
|
|
23146
|
-
return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
|
|
23147
|
-
}
|
|
23148
23260
|
var STATE_RANK = {
|
|
23149
23261
|
refused: 4,
|
|
23150
23262
|
unverified: 3,
|
|
@@ -23196,6 +23308,18 @@ function computeNotApplicable(node, graph, effectiveIds) {
|
|
|
23196
23308
|
return out;
|
|
23197
23309
|
}
|
|
23198
23310
|
|
|
23311
|
+
// src/portal/derive-pair-state.ts
|
|
23312
|
+
var PAIR_RANK = {
|
|
23313
|
+
refused: 4,
|
|
23314
|
+
unverified: 3,
|
|
23315
|
+
warning: 2,
|
|
23316
|
+
verified: 1,
|
|
23317
|
+
"n/a": 0
|
|
23318
|
+
};
|
|
23319
|
+
function worstPairState(states) {
|
|
23320
|
+
return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
|
|
23321
|
+
}
|
|
23322
|
+
|
|
23199
23323
|
// src/portal/derive-nodes.ts
|
|
23200
23324
|
function displayPairState(rawKind, status) {
|
|
23201
23325
|
if (rawKind === "verified") return "verified";
|
|
@@ -23513,28 +23637,6 @@ function buildTypes(graph) {
|
|
|
23513
23637
|
}
|
|
23514
23638
|
|
|
23515
23639
|
// src/portal/derive-rest.ts
|
|
23516
|
-
function buildBoundary(input) {
|
|
23517
|
-
if (input === null) {
|
|
23518
|
-
return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
|
|
23519
|
-
}
|
|
23520
|
-
return {
|
|
23521
|
-
phantom: dedupeEdges(input.phantom),
|
|
23522
|
-
declaredOnly: dedupeEdges(input.declaredOnly),
|
|
23523
|
-
forbiddenType: dedupeEdges(input.forbiddenType),
|
|
23524
|
-
unknown: false
|
|
23525
|
-
};
|
|
23526
|
-
}
|
|
23527
|
-
function dedupeEdges(edges) {
|
|
23528
|
-
const seen = /* @__PURE__ */ new Set();
|
|
23529
|
-
const out = [];
|
|
23530
|
-
for (const e of edges) {
|
|
23531
|
-
const key = `${e.source}\0${e.target}`;
|
|
23532
|
-
if (seen.has(key)) continue;
|
|
23533
|
-
seen.add(key);
|
|
23534
|
-
out.push(e);
|
|
23535
|
-
}
|
|
23536
|
-
return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
|
|
23537
|
-
}
|
|
23538
23640
|
function buildSuppressions(markers) {
|
|
23539
23641
|
return markers.map((m) => ({
|
|
23540
23642
|
aspectId: m.aspectId,
|
|
@@ -23576,6 +23678,30 @@ function buildWorklist(check) {
|
|
|
23576
23678
|
}));
|
|
23577
23679
|
}
|
|
23578
23680
|
|
|
23681
|
+
// src/portal/derive-boundary.ts
|
|
23682
|
+
function buildBoundary(input) {
|
|
23683
|
+
if (input === null) {
|
|
23684
|
+
return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
|
|
23685
|
+
}
|
|
23686
|
+
return {
|
|
23687
|
+
phantom: dedupeEdges(input.phantom),
|
|
23688
|
+
declaredOnly: dedupeEdges(input.declaredOnly),
|
|
23689
|
+
forbiddenType: dedupeEdges(input.forbiddenType),
|
|
23690
|
+
unknown: false
|
|
23691
|
+
};
|
|
23692
|
+
}
|
|
23693
|
+
function dedupeEdges(edges) {
|
|
23694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23695
|
+
const out = [];
|
|
23696
|
+
for (const e of edges) {
|
|
23697
|
+
const key = `${e.source}\0${e.target}`;
|
|
23698
|
+
if (seen.has(key)) continue;
|
|
23699
|
+
seen.add(key);
|
|
23700
|
+
out.push(e);
|
|
23701
|
+
}
|
|
23702
|
+
return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
|
|
23703
|
+
}
|
|
23704
|
+
|
|
23579
23705
|
// src/portal/extract.ts
|
|
23580
23706
|
async function extractPortalData(projectRoot, opts) {
|
|
23581
23707
|
const graph = await loadPortalGraph(projectRoot);
|
|
@@ -23829,6 +23955,78 @@ async function readPortalAsset(relPath) {
|
|
|
23829
23955
|
// src/portal/server/server.ts
|
|
23830
23956
|
import { createServer } from "http";
|
|
23831
23957
|
|
|
23958
|
+
// src/portal/server/boot-pages.ts
|
|
23959
|
+
var BOOT_PAGE_CSS = `
|
|
23960
|
+
:root{color-scheme:light dark;--yg-bg:#fcfcfd;--yg-surface:#f9f9fb;--yg-fg:#1c2024;--yg-muted:#60646c;--yg-accent:#0090ff;--yg-border:#cdced6}
|
|
23961
|
+
@media (prefers-color-scheme:dark){:root{--yg-bg:#111113;--yg-surface:#18191b;--yg-fg:#edeef0;--yg-muted:#b0b4ba;--yg-accent:#3b9eff;--yg-border:#43484e}}
|
|
23962
|
+
html,body{height:100%}
|
|
23963
|
+
body{margin:0;background:var(--yg-bg);color:var(--yg-fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;display:grid;place-items:center;padding:24px;box-sizing:border-box}
|
|
23964
|
+
.yg-box{display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;max-width:56ch}
|
|
23965
|
+
.yg-title{font-weight:600;font-size:16px}
|
|
23966
|
+
.yg-sub{color:var(--yg-muted);font-size:13.5px;max-width:46ch}
|
|
23967
|
+
.yg-sub code,.yg-detail{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
23968
|
+
.yg-sub code{font-size:12px;background:var(--yg-surface);border:1px solid var(--yg-border);border-radius:4px;padding:1px 5px}`;
|
|
23969
|
+
function renderLoadingShell() {
|
|
23970
|
+
return `<!doctype html>
|
|
23971
|
+
<html lang="en">
|
|
23972
|
+
<head>
|
|
23973
|
+
<meta charset="utf-8" />
|
|
23974
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23975
|
+
<title>Yggdrasil Portal</title>
|
|
23976
|
+
<style>${BOOT_PAGE_CSS}
|
|
23977
|
+
.yg-spinner{width:34px;height:34px;border-radius:50%;border:3px solid var(--yg-border);border-top-color:var(--yg-accent);animation:yg-spin .8s linear infinite}
|
|
23978
|
+
@keyframes yg-spin{to{transform:rotate(360deg)}}
|
|
23979
|
+
@media (prefers-reduced-motion:reduce){.yg-spinner{animation-duration:2.4s}}
|
|
23980
|
+
</style>
|
|
23981
|
+
</head>
|
|
23982
|
+
<body>
|
|
23983
|
+
<div class="yg-box" id="yg-boot">
|
|
23984
|
+
<div class="yg-spinner" role="status" aria-label="Loading"></div>
|
|
23985
|
+
<div class="yg-title">Reading your architecture\u2026</div>
|
|
23986
|
+
<div class="yg-sub">Checking the graph and rendering the portal. This can take a moment on a large project.</div>
|
|
23987
|
+
</div>
|
|
23988
|
+
<script>
|
|
23989
|
+
(function(){
|
|
23990
|
+
fetch('/render' + location.search, { headers: { accept: 'text/html' } })
|
|
23991
|
+
.then(function(r){ return r.text(); })
|
|
23992
|
+
.then(function(html){ document.open(); document.write(html); document.close(); })
|
|
23993
|
+
.catch(function(){
|
|
23994
|
+
var b = document.getElementById('yg-boot');
|
|
23995
|
+
if (b) { b.innerHTML =
|
|
23996
|
+
'<div class="yg-title">Couldn\\u2019t reach the portal</div>' +
|
|
23997
|
+
'<div class="yg-sub">The local portal process may have stopped. Restart it in your terminal, then reload this page.</div>'; }
|
|
23998
|
+
});
|
|
23999
|
+
})();
|
|
24000
|
+
</script>
|
|
24001
|
+
</body>
|
|
24002
|
+
</html>
|
|
24003
|
+
`;
|
|
24004
|
+
}
|
|
24005
|
+
function renderErrorPage(message) {
|
|
24006
|
+
return `<!doctype html>
|
|
24007
|
+
<html lang="en">
|
|
24008
|
+
<head>
|
|
24009
|
+
<meta charset="utf-8" />
|
|
24010
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
24011
|
+
<title>Yggdrasil Portal \u2014 couldn\u2019t load</title>
|
|
24012
|
+
<style>${BOOT_PAGE_CSS}
|
|
24013
|
+
.yg-detail{margin-top:4px;max-width:74ch;text-align:left;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--yg-muted);background:var(--yg-surface);border:1px solid var(--yg-border);border-radius:8px;padding:12px 14px}
|
|
24014
|
+
</style>
|
|
24015
|
+
</head>
|
|
24016
|
+
<body>
|
|
24017
|
+
<div class="yg-box">
|
|
24018
|
+
<div class="yg-title">The portal couldn\u2019t load your architecture</div>
|
|
24019
|
+
<div class="yg-sub">Something went wrong while reading the graph and building the page. Make sure this is a project with a <code>.yggdrasil/</code> graph, then reload. If it keeps happening, run <code>yg check</code> in your terminal to see the underlying problem.</div>
|
|
24020
|
+
<div class="yg-detail">${escapeHtmlText(message)}</div>
|
|
24021
|
+
</div>
|
|
24022
|
+
</body>
|
|
24023
|
+
</html>
|
|
24024
|
+
`;
|
|
24025
|
+
}
|
|
24026
|
+
function escapeHtmlText(s) {
|
|
24027
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
24028
|
+
}
|
|
24029
|
+
|
|
23832
24030
|
// src/portal/server/page.ts
|
|
23833
24031
|
async function freshPortalData(projectRoot, writeEnabled) {
|
|
23834
24032
|
return extractPortalData(projectRoot, { writeEnabled });
|
|
@@ -23836,6 +24034,12 @@ async function freshPortalData(projectRoot, writeEnabled) {
|
|
|
23836
24034
|
async function renderLivePage(data) {
|
|
23837
24035
|
return renderPortalPage(data);
|
|
23838
24036
|
}
|
|
24037
|
+
function loadingShell() {
|
|
24038
|
+
return renderLoadingShell();
|
|
24039
|
+
}
|
|
24040
|
+
function errorPage(message) {
|
|
24041
|
+
return renderErrorPage(message);
|
|
24042
|
+
}
|
|
23839
24043
|
async function readStaticAsset(relPath) {
|
|
23840
24044
|
return readPortalAsset(relPath);
|
|
23841
24045
|
}
|
|
@@ -23927,9 +24131,18 @@ async function handleRequest(req, res, config) {
|
|
|
23927
24131
|
const pathname = url.pathname;
|
|
23928
24132
|
try {
|
|
23929
24133
|
if (method === "GET" && pathname === "/") {
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
24134
|
+
sendText(res, 200, "text/html; charset=utf-8", loadingShell());
|
|
24135
|
+
return;
|
|
24136
|
+
}
|
|
24137
|
+
if (method === "GET" && pathname === "/render") {
|
|
24138
|
+
try {
|
|
24139
|
+
const data = await freshPortalData(config.projectRoot, config.writeEnabled);
|
|
24140
|
+
const html = await renderLivePage(data);
|
|
24141
|
+
sendText(res, 200, "text/html; charset=utf-8", html);
|
|
24142
|
+
} catch (err) {
|
|
24143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24144
|
+
sendText(res, 500, "text/html; charset=utf-8", errorPage(message));
|
|
24145
|
+
}
|
|
23933
24146
|
return;
|
|
23934
24147
|
}
|
|
23935
24148
|
if (method === "GET" && pathname === "/data") {
|