@vantageos/backend-doctor 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +57 -4
  2. package/dist/args.d.ts +1 -1
  3. package/dist/args.js +19 -1
  4. package/dist/build-stamp.json +2 -2
  5. package/dist/caller-resolution.js +11 -8
  6. package/dist/cli.js +164 -15
  7. package/dist/contract.d.ts +25 -7
  8. package/dist/contract.js +52 -7
  9. package/dist/coverage.d.ts +1 -1
  10. package/dist/coverage.js +2 -2
  11. package/dist/detectors/oracle-axis1.js +5 -4
  12. package/dist/detectors/oracle-axis2.d.ts +6 -0
  13. package/dist/detectors/oracle-axis2.js +32 -16
  14. package/dist/detectors/predicates.js +12 -3
  15. package/dist/detectors/shared.js +3 -2
  16. package/dist/detectors/source/_ast.d.ts +92 -0
  17. package/dist/detectors/source/_ast.js +353 -0
  18. package/dist/detectors/source/_scan.js +4 -3
  19. package/dist/detectors/source/backfill.js +62 -17
  20. package/dist/detectors/source/contract.js +21 -13
  21. package/dist/detectors/source/credential-hygiene.d.ts +11 -0
  22. package/dist/detectors/source/credential-hygiene.js +619 -0
  23. package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
  24. package/dist/detectors/source/cross-tenant-write.js +1408 -0
  25. package/dist/detectors/source/guard-helper-flow.d.ts +22 -0
  26. package/dist/detectors/source/guard-helper-flow.js +49 -6
  27. package/dist/detectors/source/identity-by-id.d.ts +80 -0
  28. package/dist/detectors/source/identity-by-id.js +1783 -0
  29. package/dist/detectors/source/identity.d.ts +8 -1
  30. package/dist/detectors/source/identity.js +364 -35
  31. package/dist/detectors/source/jobs.js +13 -2
  32. package/dist/detectors/source/lifecycle.js +14 -2
  33. package/dist/detectors/source/local-identity.d.ts +26 -0
  34. package/dist/detectors/source/local-identity.js +666 -0
  35. package/dist/detectors/source/narrow-then-bound.d.ts +11 -0
  36. package/dist/detectors/source/narrow-then-bound.js +571 -0
  37. package/dist/detectors/source/package-principal.d.ts +9 -0
  38. package/dist/detectors/source/package-principal.js +177 -0
  39. package/dist/detectors/source/r6-shapes.d.ts +20 -0
  40. package/dist/detectors/source/r6-shapes.js +285 -0
  41. package/dist/detectors/source/readbudget.js +291 -0
  42. package/dist/detectors/source/recipient-by-id.d.ts +40 -0
  43. package/dist/detectors/source/recipient-by-id.js +1108 -0
  44. package/dist/detectors/source/refusal.d.ts +14 -0
  45. package/dist/detectors/source/refusal.js +75 -2
  46. package/dist/detectors/source/security.js +455 -26
  47. package/dist/detectors/source/service-account-claim.d.ts +15 -0
  48. package/dist/detectors/source/service-account-claim.js +491 -0
  49. package/dist/detectors/source/writesafety.js +27 -14
  50. package/dist/detectors/standard/_doc.d.ts +1 -1
  51. package/dist/detectors/standard/_doc.js +5 -4
  52. package/dist/detectors/standard/r41-frontier.js +3 -2
  53. package/dist/detectors/standard/r46-returns-validator.js +3 -2
  54. package/dist/detectors/structural.d.ts +6 -7
  55. package/dist/detectors/structural.js +75 -43
  56. package/dist/not-checkable.d.ts +1 -1
  57. package/dist/not-checkable.js +12 -0
  58. package/dist/registry.d.ts +1 -1
  59. package/dist/registry.js +55 -3
  60. package/dist/rule-class.d.ts +85 -0
  61. package/dist/rule-class.js +218 -0
  62. package/dist/tally.d.ts +13 -5
  63. package/dist/tally.js +14 -6
  64. package/dist/violation-count.d.ts +48 -0
  65. package/dist/violation-count.js +94 -0
  66. package/fixtures/backend-standard-2825122.md +722 -0
  67. package/fixtures/backend-standard-4dcd3e0.md +722 -0
  68. package/fixtures/backend-standard-bacc54a.md +785 -0
  69. package/fixtures/rule-classes-bacc54a.json +244 -0
  70. package/package.json +1 -1
package/dist/tally.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface Verdict {
23
23
  exitCode: number;
24
24
  /** non-conforming rules bucketed by severity. */
25
25
  violations: Record<Severity, string[]>;
26
- /** could-not-judge rules bucketed by severity — abstentions, a human owes a verdict. */
26
+ /** could-not-judge rules bucketed by severity — abstentions: the instrument did not decide, and each names why. A disposition-only abstention is retired (k179jnhx); see the ABSTENTIONS note below for what remains. */
27
27
  abstentions: Record<Severity, string[]>;
28
28
  /** derived counts the wording turns on. */
29
29
  criticalViolations: number;
@@ -40,14 +40,22 @@ export interface Verdict {
40
40
  * 0 critical + 1..5 major violations -> revise recommended
41
41
  * 0 critical + >5 major violations -> revise mandatory
42
42
  * >=1 critical violation -> revise mandatory (NO bypass)
43
- * ABSTENTIONS: a could-not-judge is a human verdict owed. Any abstention blocks
44
- * "approve" (the outcome becomes at least revise-recommended); an abstention on a
45
- * CRITICAL rule can never be silently approved and is reported first.
43
+ * ABSTENTIONS: a could-not-judge means the instrument did NOT decide, and says
44
+ * why. Task k179jnhx retired judgement as an acceptance kind: a rule whose
45
+ * DISPOSITION used to wait on a person now decides on a written justification
46
+ * token's PRESENCE. What still abstains is named per rule, never relabelled:
47
+ * an unreadable input (unparseable file, unreachable tree, absent oracle), a
48
+ * rule with no detector yet (pinned NOT_MECHANICALLY_CHECKABLE), a population
49
+ * the source cannot decide statically, or a recorded marker claim the standard
50
+ * itself sends to a reviewer (R-51's write-contract marker).
51
+ * Any abstention still blocks "approve" (the outcome becomes at least
52
+ * revise-recommended); an abstention on a CRITICAL rule can never be silently
53
+ * approved and is reported first.
46
54
  *
47
55
  * EXIT CODE preserves the engine's honesty:
48
56
  * >=1 critical OR revise-mandatory -> 1 (a violation IS wrong)
49
57
  * 1..5 major violations (revise-recommended) -> 1 (hard violations exist)
50
- * no hard violation, abstention owed -> 2 (not looked = never a clean 0)
58
+ * no hard violation, input unreadable somewhere -> 2 (not read = never a clean 0)
51
59
  * approve -> 0
52
60
  * We cross-check against the engine's computeExitCode and never print "approve"
53
61
  * on an exit that means abstention.
package/dist/tally.js CHANGED
@@ -71,14 +71,22 @@ function emptyBySeverity() {
71
71
  * 0 critical + 1..5 major violations -> revise recommended
72
72
  * 0 critical + >5 major violations -> revise mandatory
73
73
  * >=1 critical violation -> revise mandatory (NO bypass)
74
- * ABSTENTIONS: a could-not-judge is a human verdict owed. Any abstention blocks
75
- * "approve" (the outcome becomes at least revise-recommended); an abstention on a
76
- * CRITICAL rule can never be silently approved and is reported first.
74
+ * ABSTENTIONS: a could-not-judge means the instrument did NOT decide, and says
75
+ * why. Task k179jnhx retired judgement as an acceptance kind: a rule whose
76
+ * DISPOSITION used to wait on a person now decides on a written justification
77
+ * token's PRESENCE. What still abstains is named per rule, never relabelled:
78
+ * an unreadable input (unparseable file, unreachable tree, absent oracle), a
79
+ * rule with no detector yet (pinned NOT_MECHANICALLY_CHECKABLE), a population
80
+ * the source cannot decide statically, or a recorded marker claim the standard
81
+ * itself sends to a reviewer (R-51's write-contract marker).
82
+ * Any abstention still blocks "approve" (the outcome becomes at least
83
+ * revise-recommended); an abstention on a CRITICAL rule can never be silently
84
+ * approved and is reported first.
77
85
  *
78
86
  * EXIT CODE preserves the engine's honesty:
79
87
  * >=1 critical OR revise-mandatory -> 1 (a violation IS wrong)
80
88
  * 1..5 major violations (revise-recommended) -> 1 (hard violations exist)
81
- * no hard violation, abstention owed -> 2 (not looked = never a clean 0)
89
+ * no hard violation, input unreadable somewhere -> 2 (not read = never a clean 0)
82
90
  * approve -> 0
83
91
  * We cross-check against the engine's computeExitCode and never print "approve"
84
92
  * on an exit that means abstention.
@@ -113,7 +121,7 @@ export function verdict(t, results, severity = deriveSeverityMap()) {
113
121
  exitCode = 1; // hard violations present — a caller must not read a clean 0
114
122
  }
115
123
  else if (abstentionTotal > 0) {
116
- // no hard violation, but a human verdict is owed (critical abstention first)
124
+ // no hard violation, but some input could not be READ (critical abstention first)
117
125
  outcome = "revise-recommended";
118
126
  exitCode = 2;
119
127
  }
@@ -125,7 +133,7 @@ export function verdict(t, results, severity = deriveSeverityMap()) {
125
133
  const criticalNote = criticalViolations > 0
126
134
  ? `${criticalViolations} CRITICAL violation(s) — revise mandatory, NO bypass.`
127
135
  : criticalAbstentions > 0
128
- ? `${criticalAbstentions} CRITICAL abstention(s) — a human verdict is owed; approve is blocked.`
136
+ ? `${criticalAbstentions} CRITICAL abstention(s) — not decided (each rule names why); approve is blocked.`
129
137
  : "no critical violation or abstention.";
130
138
  const summary = `VERDICT: ${outcome} (exit ${exitCode}) — ${criticalNote} ` +
131
139
  `major violations=${majorViolations}, abstentions=${abstentionTotal} ` +
@@ -0,0 +1,48 @@
1
+ import type { DetectorResult } from "./engine-types.js";
2
+ import type { MechanicalCount } from "./rule-class.js";
3
+ export interface ViolationCount {
4
+ /** the non-conforming count the verdict line prints. */
5
+ count: number;
6
+ /**
7
+ * per module (see moduleOf) — present for a site-based count, absent for a
8
+ * row- or finding-based one; the values always sum to `count`.
9
+ */
10
+ modules?: Record<string, number>;
11
+ }
12
+ /** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
13
+ export declare function withCount<R extends DetectorResult>(r: R, c: ViolationCount): R;
14
+ export declare function countOf(r: DetectorResult): ViolationCount | undefined;
15
+ /**
16
+ * The module a site's file belongs to. A convex/ file is its basename without
17
+ * extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
18
+ * outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
19
+ * extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
20
+ * collide with a convex basename, and every site has a module, so the split
21
+ * always sums to the count.
22
+ */
23
+ export declare function moduleOf(rel: string): string;
24
+ /** A site-based count: one item per site, split by module. */
25
+ export declare function siteCount(sites: readonly {
26
+ rel: string;
27
+ }[]): ViolationCount;
28
+ /** A row- or finding-based count: no file, so no module split. */
29
+ export declare function itemCount(n: number): ViolationCount;
30
+ /** Sum the counts of several non-conforming results of one rule (merged halves). */
31
+ export declare function sumCounts(parts: readonly (ViolationCount | undefined)[]): ViolationCount | undefined;
32
+ export interface MechanicalRuleCounts {
33
+ /** red mechanical rule id -> its non-conforming count; null when not certifiable. */
34
+ counts: Record<string, number> | null;
35
+ /** red mechanical rule id -> module -> count, for site-based rules. */
36
+ moduleCounts: Record<string, Record<string, number>> | null;
37
+ /** red mechanical rules with no attached count (the reason `counts` is null). */
38
+ uncounted: string[];
39
+ }
40
+ /**
41
+ * One entry per rule in `mech.mechanicalViolations`: the sum of the attached
42
+ * counts of that rule's non-conforming results (the tally reduces a rule to its
43
+ * worst result; when several results of a rule are red, each one's items are
44
+ * printed and counted). Null when `mech.count` is null or any red rule is uncounted.
45
+ */
46
+ export declare function mechanicalRuleCounts(results: readonly DetectorResult[], mech: MechanicalCount): MechanicalRuleCounts;
47
+ /** The human line printed under MECHANICAL VIOLATIONS. */
48
+ export declare function mechanicalRuleCountsLine(m: MechanicalRuleCounts): string;
@@ -0,0 +1,94 @@
1
+ const COUNTS = new WeakMap();
2
+ /** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
3
+ export function withCount(r, c) {
4
+ COUNTS.set(r, c);
5
+ return r;
6
+ }
7
+ export function countOf(r) {
8
+ return COUNTS.get(r);
9
+ }
10
+ /**
11
+ * The module a site's file belongs to. A convex/ file is its basename without
12
+ * extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
13
+ * outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
14
+ * extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
15
+ * collide with a convex basename, and every site has a module, so the split
16
+ * always sums to the count.
17
+ */
18
+ export function moduleOf(rel) {
19
+ const noExt = (p) => p.replace(/\.[^./]+$/, "");
20
+ if (/(^|\/)convex\//.test(rel))
21
+ return noExt(rel.split("/").pop() ?? rel);
22
+ return noExt(rel.replace(/^\.\//, ""));
23
+ }
24
+ /** A site-based count: one item per site, split by module. */
25
+ export function siteCount(sites) {
26
+ const modules = {};
27
+ for (const s of sites) {
28
+ const m = moduleOf(s.rel);
29
+ modules[m] = (modules[m] ?? 0) + 1;
30
+ }
31
+ return { count: sites.length, modules: sortKeys(modules) };
32
+ }
33
+ /** A row- or finding-based count: no file, so no module split. */
34
+ export function itemCount(n) {
35
+ return { count: n };
36
+ }
37
+ /** Sum the counts of several non-conforming results of one rule (merged halves). */
38
+ export function sumCounts(parts) {
39
+ if (parts.length === 0 || parts.some((p) => p === undefined))
40
+ return undefined;
41
+ let count = 0;
42
+ let modules = {};
43
+ for (const p of parts) {
44
+ count += p.count;
45
+ if (p.modules === undefined || modules === undefined)
46
+ modules = undefined;
47
+ else
48
+ for (const [k, v] of Object.entries(p.modules))
49
+ modules[k] = (modules[k] ?? 0) + v;
50
+ }
51
+ return modules === undefined
52
+ ? { count }
53
+ : { count, modules: sortKeys(modules) };
54
+ }
55
+ function sortKeys(o) {
56
+ return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
57
+ }
58
+ const ruleOf = (r) => /^((?:R|CR)-\d+)\b/.exec(r.id)?.[1];
59
+ /**
60
+ * One entry per rule in `mech.mechanicalViolations`: the sum of the attached
61
+ * counts of that rule's non-conforming results (the tally reduces a rule to its
62
+ * worst result; when several results of a rule are red, each one's items are
63
+ * printed and counted). Null when `mech.count` is null or any red rule is uncounted.
64
+ */
65
+ export function mechanicalRuleCounts(results, mech) {
66
+ if (mech.count === null)
67
+ return { counts: null, moduleCounts: null, uncounted: [] };
68
+ const counts = {};
69
+ const moduleCounts = {};
70
+ const uncounted = [];
71
+ for (const id of mech.mechanicalViolations) {
72
+ const red = results.filter((r) => ruleOf(r) === id && r.state === "non-conforming");
73
+ const c = sumCounts(red.map(countOf));
74
+ if (c === undefined) {
75
+ uncounted.push(id);
76
+ continue;
77
+ }
78
+ counts[id] = c.count;
79
+ if (c.modules !== undefined)
80
+ moduleCounts[id] = c.modules;
81
+ }
82
+ if (uncounted.length > 0)
83
+ return { counts: null, moduleCounts: null, uncounted };
84
+ return { counts, moduleCounts, uncounted };
85
+ }
86
+ /** The human line printed under MECHANICAL VIOLATIONS. */
87
+ export function mechanicalRuleCountsLine(m) {
88
+ if (m.counts === null)
89
+ return m.uncounted.length > 0
90
+ ? `MECHANICAL RULE COUNTS: COULD-NOT-JUDGE — no count attached for ${m.uncounted.join(", ")} (the evidence file records null)`
91
+ : "MECHANICAL RULE COUNTS: COULD-NOT-JUDGE (the mechanical count is not certified; the evidence file records null)";
92
+ const entries = Object.entries(m.counts);
93
+ return `MECHANICAL RULE COUNTS: ${entries.length ? entries.map(([id, n]) => `${id} ${n}`).join(", ") : "none"}`;
94
+ }