@ecoma-io/archkeep 0.21.0 → 0.22.1
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/cli.mjs +156 -66
- package/gate-attestation.mjs +23 -0
- package/package.json +3 -1
- package/src/analysis/analyze.mjs +6 -0
- package/src/analysis/contract.md +32 -5
- package/src/analysis/csharp.mjs +18 -0
- package/src/analysis/go.mjs +18 -0
- package/src/analysis/java.mjs +15 -0
- package/src/analysis/kotlin.mjs +15 -0
- package/src/analysis/python.mjs +25 -3
- package/src/analysis/rust.mjs +18 -0
- package/src/analysis/source-util.mjs +113 -0
- package/src/analysis/typescript.mjs +86 -5
- package/src/canonical.mjs +43 -25
- package/src/commands/README.md +63 -12
- package/src/commands/change-intent.mjs +25 -1
- package/src/commands/change.mjs +90 -40
- package/src/commands/check.mjs +65 -26
- package/src/commands/completeness.mjs +126 -19
- package/src/commands/context-command.mjs +13 -5
- package/src/commands/context.mjs +31 -4
- package/src/commands/coverage-verdict.mjs +191 -0
- package/src/commands/debt.mjs +18 -15
- package/src/commands/delta-classify.mjs +13 -18
- package/src/commands/delta-snapshot.mjs +13 -5
- package/src/commands/delta.mjs +95 -33
- package/src/commands/diff.mjs +31 -24
- package/src/commands/discover.mjs +70 -29
- package/src/commands/drift.mjs +21 -21
- package/src/commands/edge-constraints.mjs +47 -1
- package/src/commands/evaluation-primitives.mjs +194 -2
- package/src/commands/evolution.mjs +27 -10
- package/src/commands/explain.mjs +14 -13
- package/src/commands/fitness.mjs +20 -19
- package/src/commands/graph.mjs +29 -11
- package/src/commands/health.mjs +12 -5
- package/src/commands/history.mjs +41 -26
- package/src/commands/impact.mjs +17 -18
- package/src/commands/plan-context-command.mjs +10 -5
- package/src/commands/reconcile.mjs +14 -17
- package/src/commands/scenario-evaluation.mjs +93 -16
- package/src/commands/scenario.mjs +28 -18
- package/src/commands/waivers.mjs +36 -28
- package/src/governance/evolution-event.mjs +96 -9
- package/src/intent/intent-manifest.json +83 -39
- package/src/lsp/diagnose.mjs +12 -3
- package/src/report/discover-text.mjs +31 -9
- package/src/report/graph-text.mjs +25 -5
- package/src/report/json.mjs +32 -5
- package/src/report/text.mjs +82 -12
- package/src/verdict.mjs +78 -36
- package/src/verify-gate-attestation.mjs +323 -0
- package/src/workspace.mjs +126 -2
package/cli.mjs
CHANGED
|
@@ -923,13 +923,20 @@ async function runDiff(options, { cwd, env }) {
|
|
|
923
923
|
// the mechanism and the threat it closes.
|
|
924
924
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
925
925
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
926
|
-
|
|
926
|
+
// A refused run still writes the report — but "diff complete" would deny
|
|
927
|
+
// the refusal that report carries (#608).
|
|
928
|
+
if (result.status === "no-verdict") {
|
|
929
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
930
|
+
} else {
|
|
931
|
+
env.err(`archkeep: diff complete → ${options.output}`);
|
|
932
|
+
}
|
|
927
933
|
} else {
|
|
928
934
|
env.out(report);
|
|
929
935
|
}
|
|
930
936
|
|
|
931
|
-
// Diff is descriptive: 0 when it completes, never 1
|
|
932
|
-
|
|
937
|
+
// Diff is descriptive: 0 when it completes, never 1 — and an incomplete
|
|
938
|
+
// head is the structured no-verdict envelope's exit 3, not a clean 0 (#608).
|
|
939
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
933
940
|
}
|
|
934
941
|
|
|
935
942
|
/**
|
|
@@ -1003,19 +1010,31 @@ async function runDelta(options, { cwd, env }) {
|
|
|
1003
1010
|
return usageError ? EXIT.usage : EXIT.error;
|
|
1004
1011
|
}
|
|
1005
1012
|
|
|
1013
|
+
// A coverage-refused compare (#608) answers every `--format` with the
|
|
1014
|
+
// envelope: a withheld verdict has no findings, so there is no SARIF log
|
|
1015
|
+
// and no text table to render — and `result.report.sarif` does not even
|
|
1016
|
+
// exist on the refusal, which would crash the ternary below into a
|
|
1017
|
+
// TypeError where the contract says exit 3.
|
|
1006
1018
|
const report =
|
|
1007
|
-
|
|
1019
|
+
result.status === "no-verdict"
|
|
1008
1020
|
? result.report.json
|
|
1009
|
-
: options.format === "
|
|
1010
|
-
? result.report.
|
|
1011
|
-
:
|
|
1021
|
+
: options.format === "json"
|
|
1022
|
+
? result.report.json
|
|
1023
|
+
: options.format === "sarif"
|
|
1024
|
+
? result.report.sarif
|
|
1025
|
+
: result.report.text;
|
|
1012
1026
|
|
|
1013
1027
|
if (options.output) {
|
|
1014
1028
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
1015
|
-
// the mechanism and the threat it closes.
|
|
1029
|
+
// the mechanism and the threat it closes. A refused run still writes the
|
|
1030
|
+
// report — but "delta complete" would deny the refusal it carries (#608).
|
|
1016
1031
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1017
1032
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
1018
|
-
|
|
1033
|
+
if (result.status === "no-verdict") {
|
|
1034
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1035
|
+
} else {
|
|
1036
|
+
env.err(`archkeep: delta complete → ${options.output}`);
|
|
1037
|
+
}
|
|
1019
1038
|
} else {
|
|
1020
1039
|
env.out(report);
|
|
1021
1040
|
}
|
|
@@ -1023,8 +1042,10 @@ async function runDelta(options, { cwd, env }) {
|
|
|
1023
1042
|
// The event the run recorded, when `--event-out` was given — the store's
|
|
1024
1043
|
// own `duplicate` answer, so a rerun over the same transition says so
|
|
1025
1044
|
// instead of implying a second event was appended. Capture mode refuses the
|
|
1026
|
-
// flag upstream; this line runs only for a compare.
|
|
1027
|
-
|
|
1045
|
+
// flag upstream; this line runs only for a compare. Truthy, not `!== null`:
|
|
1046
|
+
// a coverage-refused run (#608) returns no `eventWrite` key at all, and a
|
|
1047
|
+
// truthiness test is the one form that reads both shapes safely.
|
|
1048
|
+
if (result.eventWrite) {
|
|
1028
1049
|
env.err(
|
|
1029
1050
|
`archkeep: evolution event ${
|
|
1030
1051
|
result.eventWrite.duplicate ? "duplicate, already recorded" : "recorded"
|
|
@@ -1105,16 +1126,24 @@ async function runDrift(options, { cwd, env }) {
|
|
|
1105
1126
|
// `drift` has no `--config` flag (`DRIFT_FLAG_HELP`) — there is no
|
|
1106
1127
|
// override to pass, only the workspace's un-overridden default to guard.
|
|
1107
1128
|
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1129
|
+
// A refused run carries no `drift` payload to summarize — the numbers
|
|
1130
|
+
// would be `undefined` — so the stderr line names the refusal instead.
|
|
1131
|
+
if (result.status === "no-verdict") {
|
|
1132
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
env.err(
|
|
1135
|
+
`archkeep: ${result.drift.observed.projects} projects, ${result.drift.observed.edges} edges ` +
|
|
1136
|
+
`→ ${options.output}`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1112
1139
|
} else {
|
|
1113
1140
|
env.out(report);
|
|
1114
1141
|
}
|
|
1115
1142
|
|
|
1116
|
-
// Drift is descriptive: 0 when the comparison completes, never 1
|
|
1117
|
-
|
|
1143
|
+
// Drift is descriptive: 0 when the comparison completes, never 1 — and an
|
|
1144
|
+
// incomplete analysis is the structured no-verdict envelope's exit 3
|
|
1145
|
+
// (#608), not a clean 0.
|
|
1146
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1118
1147
|
}
|
|
1119
1148
|
|
|
1120
1149
|
/**
|
|
@@ -1212,16 +1241,22 @@ async function runReconcile(options, { cwd, env }) {
|
|
|
1212
1241
|
// `reconcile` has no `--config` flag (`RECONCILE_FLAG_HELP`) — there is no
|
|
1213
1242
|
// override to pass, only the workspace's un-overridden default to guard.
|
|
1214
1243
|
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1244
|
+
// A refused run carries no `reconcile` payload to summarize (#608).
|
|
1245
|
+
if (result.status === "no-verdict") {
|
|
1246
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1247
|
+
} else {
|
|
1248
|
+
env.err(
|
|
1249
|
+
`archkeep: ${result.reconcile.observed.projects} projects, ${result.reconcile.observed.edges} edges ` +
|
|
1250
|
+
`→ ${options.output}`,
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1219
1253
|
} else {
|
|
1220
1254
|
env.out(report);
|
|
1221
1255
|
}
|
|
1222
1256
|
|
|
1223
|
-
// Reconcile is descriptive: 0 when the comparison completes, never 1
|
|
1224
|
-
|
|
1257
|
+
// Reconcile is descriptive: 0 when the comparison completes, never 1 — and
|
|
1258
|
+
// an incomplete analysis is the no-verdict envelope's exit 3 (#608).
|
|
1259
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1225
1260
|
}
|
|
1226
1261
|
|
|
1227
1262
|
/**
|
|
@@ -1320,12 +1355,18 @@ async function runChange(options, { cwd, env }) {
|
|
|
1320
1355
|
// the mechanism and the threat it closes.
|
|
1321
1356
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1322
1357
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1358
|
+
// A coverage-refused run carries no `changeIntent` payload to summarize
|
|
1359
|
+
// (#608) — the refusal's own clause list is in the written report.
|
|
1360
|
+
if (result.status === "no-verdict") {
|
|
1361
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1362
|
+
} else {
|
|
1363
|
+
env.err(
|
|
1364
|
+
`archkeep: change ${result.changeIntent.reconciliation.verdict} ` +
|
|
1365
|
+
`(+${result.changeIntent.reconciliation.matched.length} matched, ` +
|
|
1366
|
+
`!${result.changeIntent.reconciliation.unexpected.length} undeclared, ` +
|
|
1367
|
+
`?${result.changeIntent.reconciliation.missingExpected.length} unfulfilled) → ${options.output}`,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1329
1370
|
} else {
|
|
1330
1371
|
env.out(report);
|
|
1331
1372
|
}
|
|
@@ -1396,22 +1437,29 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1396
1437
|
// only glances at this confirmation must not see "N waivers on the
|
|
1397
1438
|
// table" and conclude that is the whole surface when a
|
|
1398
1439
|
// `boundarySuppressions` row with no `expiresAt` is hiding something the
|
|
1399
|
-
// file --output just wrote down.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1440
|
+
// file --output just wrote down. A coverage-refused run carries no
|
|
1441
|
+
// `waivers` payload at all (#608) — the line names the refusal instead.
|
|
1442
|
+
if (result.status === "no-verdict") {
|
|
1443
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1444
|
+
} else {
|
|
1445
|
+
const suppressionNote =
|
|
1446
|
+
result.waivers.suppressions.length > 0
|
|
1447
|
+
? `, ${result.waivers.suppressions.length} permanent suppression` +
|
|
1448
|
+
`${result.waivers.suppressions.length === 1 ? "" : "s"}`
|
|
1449
|
+
: "";
|
|
1450
|
+
env.err(
|
|
1451
|
+
`archkeep: ${result.waivers.waivers.length} waivers${suppressionNote} on the table ` +
|
|
1452
|
+
`→ ${options.output}`,
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1409
1455
|
} else {
|
|
1410
1456
|
env.out(report);
|
|
1411
1457
|
}
|
|
1412
1458
|
|
|
1413
|
-
// Waivers is descriptive: 0 when the surface could be read, never 1
|
|
1414
|
-
|
|
1459
|
+
// Waivers is descriptive: 0 when the surface could be read, never 1 — and
|
|
1460
|
+
// an incomplete analysis is the no-verdict envelope's exit 3 (#608), the
|
|
1461
|
+
// same tree `check` refuses.
|
|
1462
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1415
1463
|
}
|
|
1416
1464
|
|
|
1417
1465
|
/**
|
|
@@ -1469,11 +1517,16 @@ async function runFitness(options, { cwd, env }) {
|
|
|
1469
1517
|
// the mechanism and the threat it closes.
|
|
1470
1518
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1471
1519
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1520
|
+
// A coverage-refused run carries no `fitness` payload to summarize (#608).
|
|
1521
|
+
if (result.status === "no-verdict") {
|
|
1522
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1523
|
+
} else {
|
|
1524
|
+
env.err(
|
|
1525
|
+
`archkeep: ${result.fitness.functions.length} fitness function` +
|
|
1526
|
+
`${result.fitness.functions.length === 1 ? "" : "s"} judged (${result.fitness.verdict}) ` +
|
|
1527
|
+
`→ ${options.output}`,
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1477
1530
|
} else {
|
|
1478
1531
|
env.out(report);
|
|
1479
1532
|
}
|
|
@@ -1535,18 +1588,24 @@ async function runImpact(options, { cwd, env }) {
|
|
|
1535
1588
|
// the mechanism and the threat it closes.
|
|
1536
1589
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1537
1590
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1591
|
+
// A coverage-refused run carries no `impact` payload to summarize (#608).
|
|
1592
|
+
if (result.status === "no-verdict") {
|
|
1593
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1594
|
+
} else {
|
|
1595
|
+
env.err(
|
|
1596
|
+
`archkeep: ${result.impact.dependents.length} project` +
|
|
1597
|
+
`${result.impact.dependents.length === 1 ? "" : "s"}` +
|
|
1598
|
+
`${result.impact.dependents.length === 1 ? " depends" : " depend"} on ${projectName} ` +
|
|
1599
|
+
`→ ${options.output}`,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1544
1602
|
} else {
|
|
1545
1603
|
env.out(report);
|
|
1546
1604
|
}
|
|
1547
1605
|
|
|
1548
|
-
// Impact is descriptive: 0 when it completes, never 1
|
|
1549
|
-
|
|
1606
|
+
// Impact is descriptive: 0 when it completes, never 1 — and an incomplete
|
|
1607
|
+
// analysis is the no-verdict envelope's exit 3 (#608).
|
|
1608
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1550
1609
|
}
|
|
1551
1610
|
/**
|
|
1552
1611
|
* `scenario`'s `run`: resolves the command context, reads the scenario file,
|
|
@@ -1606,13 +1665,20 @@ async function runScenario(options, { cwd, env }) {
|
|
|
1606
1665
|
|
|
1607
1666
|
if (options.output) {
|
|
1608
1667
|
if (!writeOutputReport(options.output, report, env, cwd, options.config)) return EXIT.error;
|
|
1609
|
-
|
|
1668
|
+
// A refused run still writes the report — but "complete" would deny the
|
|
1669
|
+
// refusal that report carries (#608).
|
|
1670
|
+
if (result.status === "no-verdict") {
|
|
1671
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
1672
|
+
} else {
|
|
1673
|
+
env.err(`archkeep: scenario for "${projectName}" complete → ${options.output}`);
|
|
1674
|
+
}
|
|
1610
1675
|
} else {
|
|
1611
1676
|
env.out(report);
|
|
1612
1677
|
}
|
|
1613
1678
|
|
|
1614
|
-
// Scenario is descriptive: 0 when it completes, never 1
|
|
1615
|
-
|
|
1679
|
+
// Scenario is descriptive: 0 when it completes, never 1 — and an incomplete
|
|
1680
|
+
// analysis is the no-verdict envelope's exit 3 (#608).
|
|
1681
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1616
1682
|
}
|
|
1617
1683
|
|
|
1618
1684
|
/**
|
|
@@ -2259,16 +2325,22 @@ async function runDebt(options, { cwd, env }) {
|
|
|
2259
2325
|
// the mechanism and the threat it closes.
|
|
2260
2326
|
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
2261
2327
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2328
|
+
// A coverage-refused run carries no `ledger` payload to summarize (#608).
|
|
2329
|
+
if (result.status === "no-verdict") {
|
|
2330
|
+
env.err(`archkeep: no verdict — the coverage refusal is in the report → ${options.output}`);
|
|
2331
|
+
} else {
|
|
2332
|
+
env.err(
|
|
2333
|
+
`archkeep: ${result.ledger.total} debt entr` +
|
|
2334
|
+
`${result.ledger.total === 1 ? "y" : "ies"} → ${options.output}`,
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2266
2337
|
} else {
|
|
2267
2338
|
env.out(report);
|
|
2268
2339
|
}
|
|
2269
2340
|
|
|
2270
|
-
// Debt is descriptive: 0 when the ledger completes, never 1
|
|
2271
|
-
|
|
2341
|
+
// Debt is descriptive: 0 when the ledger completes, never 1 — and an
|
|
2342
|
+
// incomplete analysis is the no-verdict envelope's exit 3 (#608).
|
|
2343
|
+
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
2272
2344
|
}
|
|
2273
2345
|
|
|
2274
2346
|
/**
|
|
@@ -2330,10 +2402,27 @@ async function runDiscover(options, { cwd, env }) {
|
|
|
2330
2402
|
}
|
|
2331
2403
|
|
|
2332
2404
|
if (options.writeIntent) {
|
|
2405
|
+
// The one write that can turn a proposal into the law `check` gates on,
|
|
2406
|
+
// so it refuses to replace: a file already at the target is a law (or a
|
|
2407
|
+
// candidate someone holds), and silently overwriting it with a proposal
|
|
2408
|
+
// is the adoption this command must never perform by itself. Move or
|
|
2409
|
+
// delete the file first — a step a human reviews.
|
|
2410
|
+
if (existsSync(options.writeIntent)) {
|
|
2411
|
+
env.err(
|
|
2412
|
+
`archkeep: ${options.writeIntent} already exists, and a proposal must never ` +
|
|
2413
|
+
`silently replace what is there. Move or delete the file first, then run this again.`,
|
|
2414
|
+
);
|
|
2415
|
+
return EXIT.error;
|
|
2416
|
+
}
|
|
2333
2417
|
try {
|
|
2334
2418
|
const intentJson = JSON.stringify(proposalToIntent(result.proposal), null, 2) + "\n";
|
|
2335
|
-
|
|
2419
|
+
// `wx` refuses the file materializing between the check above and this
|
|
2420
|
+
// write, so the refusal above cannot be raced past.
|
|
2421
|
+
writeFileSync(options.writeIntent, intentJson, { encoding: "utf-8", flag: "wx" });
|
|
2336
2422
|
env.err(`archkeep: proposed architecture written to ${options.writeIntent}`);
|
|
2423
|
+
env.err(
|
|
2424
|
+
"archkeep: this file is a proposal, not the law — review it as a diff before it governs anything.",
|
|
2425
|
+
);
|
|
2337
2426
|
} catch (error) {
|
|
2338
2427
|
env.err(`archkeep: failed to write intent file: ${error.message}`);
|
|
2339
2428
|
return EXIT.error;
|
|
@@ -2799,8 +2888,9 @@ const DISCOVER_FLAG_HELP = Object.freeze([
|
|
|
2799
2888
|
describe: Object.freeze([
|
|
2800
2889
|
"Write the proposed components and rules to a valid",
|
|
2801
2890
|
"architecture-intent.json file. Only valid with --propose;",
|
|
2802
|
-
"
|
|
2803
|
-
"
|
|
2891
|
+
"refuses to overwrite a file that already exists — the file",
|
|
2892
|
+
"is a candidate for drift/reconcile and must be reviewed",
|
|
2893
|
+
"before use",
|
|
2804
2894
|
]),
|
|
2805
2895
|
}),
|
|
2806
2896
|
Object.freeze({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate-attestation verifier's public face — what a consumer imports from
|
|
3
|
+
* this package's `./gate-attestation` subpath to validate an attestation with
|
|
4
|
+
* the version it actually installed.
|
|
5
|
+
*
|
|
6
|
+
* It holds no logic on purpose, the same bargain `./commands` (`commands.mjs`)
|
|
7
|
+
* and `./nx` (`nx.mjs`) make: a named entry that is a re-export, so the
|
|
8
|
+
* verifier can grow under `src/` without a second copy of any decision
|
|
9
|
+
* appearing beside it.
|
|
10
|
+
*
|
|
11
|
+
* ```js
|
|
12
|
+
* import { validateGateAttestation, readGateAttestation }
|
|
13
|
+
* from "@ecoma-io/archkeep/gate-attestation";
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export {
|
|
17
|
+
ATTESTED_PACKAGE,
|
|
18
|
+
GATE_ATTESTATION_SCHEMA_VERSION,
|
|
19
|
+
readGateAttestation,
|
|
20
|
+
reachesCheckVerdict,
|
|
21
|
+
validateGateAttestation,
|
|
22
|
+
verifiedAdopters,
|
|
23
|
+
} from "./src/verify-gate-attestation.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecoma-io/archkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript, Vue, Java and Kotlin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
".": "./index.mjs",
|
|
30
30
|
"./nx": "./nx.mjs",
|
|
31
31
|
"./commands": "./commands.mjs",
|
|
32
|
+
"./gate-attestation": "./gate-attestation.mjs",
|
|
32
33
|
"./presets/*.json": "./presets/*.json",
|
|
33
34
|
"./package.json": "./package.json"
|
|
34
35
|
},
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"index.mjs",
|
|
37
38
|
"nx.mjs",
|
|
38
39
|
"commands.mjs",
|
|
40
|
+
"gate-attestation.mjs",
|
|
39
41
|
"cli.mjs",
|
|
40
42
|
"lsp.mjs",
|
|
41
43
|
"src/",
|
package/src/analysis/analyze.mjs
CHANGED
|
@@ -121,6 +121,12 @@ export { LANGUAGE_BY_EXTENSION, languageOf };
|
|
|
121
121
|
* the file as a whole rather than one position.
|
|
122
122
|
* @property {number|null} column 1-based, or `null`.
|
|
123
123
|
* @property {string} reason Human-readable; written to be read in a report.
|
|
124
|
+
* @property {true} [dynamic] The site's target is computed at runtime — the
|
|
125
|
+
* language itself declines a static answer. Disclosed without withholding
|
|
126
|
+
* (`isDynamicSiteFailure`).
|
|
127
|
+
* @property {true} [external] The site names the external dependency universe
|
|
128
|
+
* rather than the governed graph. Disclosed without withholding
|
|
129
|
+
* (`isExternalSiteFailure`); every analyzer sets it (#603).
|
|
124
130
|
*/
|
|
125
131
|
|
|
126
132
|
/**
|
package/src/analysis/contract.md
CHANGED
|
@@ -160,16 +160,36 @@ which one it was:
|
|
|
160
160
|
- A **literal package import that names no declared project** — an uninstalled
|
|
161
161
|
third-party package, a dependency of some other workspace — is a normal,
|
|
162
162
|
permanent state: a workspace with packages is not a missing workspace edge.
|
|
163
|
-
It stays a POSITIONED failure (`line`/`column` set), the "blind spot"
|
|
164
|
-
|
|
163
|
+
It stays a POSITIONED failure (`line`/`column` set), the "blind spot", and
|
|
164
|
+
it carries `external: true`: the specifier asked the external dependency
|
|
165
|
+
universe, not the governed graph, and its resolvability is a property of
|
|
166
|
+
the installed dependency tree a workspace legitimately may not have (a
|
|
167
|
+
fresh clone, a trimmed install, the native self-check's `git archive`
|
|
168
|
+
copy). Disclosed, verdict-neutral — withholding over it would make a tree
|
|
169
|
+
permanently un-green over dependencies nobody crossed.
|
|
170
|
+
- A **literal that references the workspace's own surface and still fails to
|
|
171
|
+
resolve** — a path-like specifier (`./`, `../`, `/`), a `#` subpath, a
|
|
172
|
+
`paths` alias — is the other positioned class, and the one that withholds
|
|
173
|
+
the run's verdict since the loud-coverage contract (#595): the resolver was
|
|
174
|
+
asked a concrete question about the governed graph and could not answer,
|
|
175
|
+
and #595's own reported shape (`#canary-review/index.mjs`, a subpath naming
|
|
176
|
+
a declared project) is exactly this class — `packageNameOf` keeps the `#`,
|
|
177
|
+
so the whole-file name test cannot catch it. `check` exits 3 over it
|
|
178
|
+
rather than reading as a pass.
|
|
165
179
|
- A **dynamic import with a non-literal argument** — `import(somePath)`, or an
|
|
166
180
|
`import()` whose argument is a template literal interpolating a variable — is
|
|
167
181
|
the recurring permanent case. The site is real, the target is not knowable
|
|
168
182
|
statically, and the honest answer is one record with `kind: "dynamic"`, the
|
|
169
183
|
source text of the argument as `specifier`, and a POSITIONED failure
|
|
170
|
-
(`line`/`column` set)
|
|
171
|
-
reader can see this one site in the report's
|
|
172
|
-
does not
|
|
184
|
+
(`line`/`column` set) carrying `dynamic: true`. The rest of the file's
|
|
185
|
+
imports were still judged; a reader can see this one site in the report's
|
|
186
|
+
blind-spot section. It does not withhold the verdict: a non-literal argument
|
|
187
|
+
is the language itself declaring the target computed at runtime, a limit
|
|
188
|
+
static analysis cannot close, so the run's pass claim covers the statically
|
|
189
|
+
judgeable surface. The `dynamic` field is what keeps the envelope builder
|
|
190
|
+
from reading this row as unjudged work (`report/json.mjs`'s completeness
|
|
191
|
+
law) — the permanent classes are disclosed alike and judged differently,
|
|
192
|
+
and the fields are the only things that tell them apart.
|
|
173
193
|
|
|
174
194
|
Silently dropping any of them is how a boundary gets bypassed.
|
|
175
195
|
|
|
@@ -207,6 +227,13 @@ fails loudly rather than reporting an empty result that reads as "clean".
|
|
|
207
227
|
|
|
208
228
|
`failures` carries `{ sourceFile, line, column, reason }`; `line`/`column` are
|
|
209
229
|
`null` when the failure is about the file as a whole rather than one position.
|
|
230
|
+
A positioned failure produced from a non-literal `import()`/`require()`
|
|
231
|
+
argument additionally carries `dynamic: true`, and one produced from an
|
|
232
|
+
unresolvable bare-package specifier carries `external: true` — the fields the
|
|
233
|
+
verdict and envelope layers read to tell the declared dynamic limit and the
|
|
234
|
+
external dependency universe apart from an unresolvable specifier that
|
|
235
|
+
references the workspace's own surface, which is the class that withholds the
|
|
236
|
+
verdict.
|
|
210
237
|
Both arrays are always present and always arrays — a consumer never has to
|
|
211
238
|
check for `undefined` before iterating.
|
|
212
239
|
|
package/src/analysis/csharp.mjs
CHANGED
|
@@ -429,6 +429,24 @@ export function analyzeCSharp({ sourceFile, text, workspace }) {
|
|
|
429
429
|
external: true,
|
|
430
430
|
packageName: site.importableName,
|
|
431
431
|
};
|
|
432
|
+
// The bare-coordinate class the contract discloses without
|
|
433
|
+
// withholding (#603): the namespace names the external dependency
|
|
434
|
+
// universe, not the governed graph, so the site is DISCLOSED — a
|
|
435
|
+
// positioned row carrying `external: true`
|
|
436
|
+
// (`isExternalSiteFailure`), the run's verdict untouched — rather
|
|
437
|
+
// than swallowed, the same classification the TypeScript analyzer
|
|
438
|
+
// already emits. A name a tracked namespace claims resolved through
|
|
439
|
+
// the index above and never reaches this branch; the split-package
|
|
440
|
+
// branch below keeps withholding. The `importableName === null`
|
|
441
|
+
// arm above is the extern-alias construct, not an unresolvable
|
|
442
|
+
// coordinate, and stays unfailed.
|
|
443
|
+
result.failures.push({
|
|
444
|
+
sourceFile,
|
|
445
|
+
line,
|
|
446
|
+
column,
|
|
447
|
+
reason: `C# cannot resolve '${site.importableName}' from '${sourceFile}'`,
|
|
448
|
+
external: true,
|
|
449
|
+
});
|
|
432
450
|
} else if (resolved.ambiguous) {
|
|
433
451
|
resolution = null;
|
|
434
452
|
result.failures.push({
|
package/src/analysis/go.mjs
CHANGED
|
@@ -704,6 +704,24 @@ export function analyzeGo({ sourceFile, text, workspace }) {
|
|
|
704
704
|
packageName: target === null ? site.specifier : null,
|
|
705
705
|
},
|
|
706
706
|
});
|
|
707
|
+
// The bare-module class the contract discloses without withholding
|
|
708
|
+
// (#603): the import names the external dependency universe, not the
|
|
709
|
+
// governed graph, so the site is DISCLOSED — a positioned row carrying
|
|
710
|
+
// `external: true` (`isExternalSiteFailure`), the run's verdict
|
|
711
|
+
// untouched — rather than swallowed. An empty blind-spot list must mean
|
|
712
|
+
// "nothing to disclose", and a workspace whose imports reach outside it
|
|
713
|
+
// has things to disclose. An import naming a declared module resolved
|
|
714
|
+
// above and never reaches this branch; the same classification the
|
|
715
|
+
// TypeScript analyzer already emits.
|
|
716
|
+
if (target === null) {
|
|
717
|
+
result.failures.push({
|
|
718
|
+
sourceFile,
|
|
719
|
+
line,
|
|
720
|
+
column,
|
|
721
|
+
reason: `Go cannot resolve '${site.specifier}' from '${sourceFile}'`,
|
|
722
|
+
external: true,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
707
725
|
}
|
|
708
726
|
} catch (cause) {
|
|
709
727
|
result.failures.push(fileFailure(sourceFile, `Go analysis failed: ${cause?.message ?? cause}`));
|
package/src/analysis/java.mjs
CHANGED
|
@@ -235,6 +235,21 @@ export function analyzeJava({ sourceFile, text, workspace }) {
|
|
|
235
235
|
external: true,
|
|
236
236
|
packageName: site.importableName,
|
|
237
237
|
};
|
|
238
|
+
// The bare-coordinate class the contract discloses without withholding
|
|
239
|
+
// (#603): the dotted name names the external dependency universe, not
|
|
240
|
+
// the governed graph, so the site is DISCLOSED — a positioned row
|
|
241
|
+
// carrying `external: true` (`isExternalSiteFailure`), the run's
|
|
242
|
+
// verdict untouched — rather than swallowed, the same classification
|
|
243
|
+
// the TypeScript analyzer already emits. A name a tracked package
|
|
244
|
+
// prefix claims resolved through the index above and never reaches
|
|
245
|
+
// this branch; the split-package branch below keeps withholding.
|
|
246
|
+
result.failures.push({
|
|
247
|
+
sourceFile,
|
|
248
|
+
line,
|
|
249
|
+
column,
|
|
250
|
+
reason: `Java cannot resolve '${site.importableName}' from '${sourceFile}'`,
|
|
251
|
+
external: true,
|
|
252
|
+
});
|
|
238
253
|
} else if (resolved.ambiguous) {
|
|
239
254
|
// Split package: unresolvable by static reading, so `resolved` is
|
|
240
255
|
// null WITH a positioned failure naming every claimant — the Python
|
package/src/analysis/kotlin.mjs
CHANGED
|
@@ -184,6 +184,21 @@ export function analyzeKotlin({ sourceFile, text, workspace }) {
|
|
|
184
184
|
external: true,
|
|
185
185
|
packageName: site.importableName,
|
|
186
186
|
};
|
|
187
|
+
// The bare-coordinate class the contract discloses without withholding
|
|
188
|
+
// (#603): the dotted name names the external dependency universe, not
|
|
189
|
+
// the governed graph, so the site is DISCLOSED — a positioned row
|
|
190
|
+
// carrying `external: true` (`isExternalSiteFailure`), the run's
|
|
191
|
+
// verdict untouched — rather than swallowed, the same classification
|
|
192
|
+
// the TypeScript analyzer already emits. A name a tracked package
|
|
193
|
+
// prefix claims resolved through the index above and never reaches
|
|
194
|
+
// this branch; the split-package branch below keeps withholding.
|
|
195
|
+
result.failures.push({
|
|
196
|
+
sourceFile,
|
|
197
|
+
line,
|
|
198
|
+
column,
|
|
199
|
+
reason: `Kotlin cannot resolve '${site.importableName}' from '${sourceFile}'`,
|
|
200
|
+
external: true,
|
|
201
|
+
});
|
|
187
202
|
} else if (resolved.ambiguous) {
|
|
188
203
|
resolution = null;
|
|
189
204
|
result.failures.push({
|
package/src/analysis/python.mjs
CHANGED
|
@@ -1243,10 +1243,15 @@ export function analyzePython({ sourceFile, text, workspace }) {
|
|
|
1243
1243
|
continue;
|
|
1244
1244
|
}
|
|
1245
1245
|
if (!site.literal) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1246
|
+
result.failures.push({
|
|
1247
|
+
sourceFile,
|
|
1248
|
+
line,
|
|
1249
|
+
column,
|
|
1250
|
+
reason:
|
|
1251
|
+
`dynamic import of '${site.specifier}' has a non-literal argument, ` +
|
|
1248
1252
|
`so its target is not knowable statically`,
|
|
1249
|
-
|
|
1253
|
+
dynamic: true,
|
|
1254
|
+
});
|
|
1250
1255
|
continue;
|
|
1251
1256
|
}
|
|
1252
1257
|
|
|
@@ -1293,6 +1298,23 @@ export function analyzePython({ sourceFile, text, workspace }) {
|
|
|
1293
1298
|
external: true,
|
|
1294
1299
|
packageName: absolute.split(".")[0],
|
|
1295
1300
|
};
|
|
1301
|
+
// The bare-package class the contract discloses without withholding
|
|
1302
|
+
// (#603): every layout is modelled and the name still reaches no
|
|
1303
|
+
// tracked package, so the external answer is the language's, and the
|
|
1304
|
+
// site is DISCLOSED — a positioned row carrying `external: true`
|
|
1305
|
+
// (`isExternalSiteFailure`), the run's verdict untouched — rather
|
|
1306
|
+
// than swallowed, the same classification the TypeScript analyzer
|
|
1307
|
+
// already emits. The unmodelled branch above keeps withholding,
|
|
1308
|
+
// because there the external answer is NOT the language's; the
|
|
1309
|
+
// relative-import and ambiguous branches keep withholding because
|
|
1310
|
+
// those names the workspace's own surface.
|
|
1311
|
+
result.failures.push({
|
|
1312
|
+
sourceFile,
|
|
1313
|
+
line,
|
|
1314
|
+
column,
|
|
1315
|
+
reason: `Python cannot resolve '${site.specifier}' from '${sourceFile}'`,
|
|
1316
|
+
external: true,
|
|
1317
|
+
});
|
|
1296
1318
|
} else if (resolution.ambiguous) {
|
|
1297
1319
|
fail(
|
|
1298
1320
|
`'${site.specifier}' resolves through the namespace package '${resolution.prefix}', which ` +
|
package/src/analysis/rust.mjs
CHANGED
|
@@ -804,6 +804,24 @@ export function analyzeRust({ sourceFile, text, workspace }) {
|
|
|
804
804
|
// so a `bannedExternalImports` glob is written against this form.
|
|
805
805
|
packageName: target === null ? site.root : null,
|
|
806
806
|
};
|
|
807
|
+
// The bare-crate class the contract discloses without withholding
|
|
808
|
+
// (#603): the `use` names the external dependency universe, not the
|
|
809
|
+
// governed graph, so the site is DISCLOSED — a positioned row carrying
|
|
810
|
+
// `external: true` (`isExternalSiteFailure`), the run's verdict
|
|
811
|
+
// untouched — rather than swallowed, the same classification the
|
|
812
|
+
// TypeScript analyzer already emits. A name a workspace crate declares
|
|
813
|
+
// resolved above and never reaches this branch; the brace-group
|
|
814
|
+
// malformation above keeps withholding, because there the name itself
|
|
815
|
+
// was never read.
|
|
816
|
+
if (target === null) {
|
|
817
|
+
result.failures.push({
|
|
818
|
+
sourceFile,
|
|
819
|
+
line,
|
|
820
|
+
column,
|
|
821
|
+
reason: `Rust cannot resolve '${site.specifier}' from '${sourceFile}'`,
|
|
822
|
+
external: true,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
807
825
|
}
|
|
808
826
|
result.imports.push({
|
|
809
827
|
sourceFile,
|