audit-tools 0.49.0 → 0.50.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.
Files changed (108) hide show
  1. package/README.md +3 -1
  2. package/dist/audit/cli/args.d.ts +27 -0
  3. package/dist/audit/cli/args.d.ts.map +1 -1
  4. package/dist/audit/cli/args.js +32 -2
  5. package/dist/audit/cli/args.js.map +1 -1
  6. package/dist/audit/cli/dispatch/hostHandoff.d.ts +1 -1
  7. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/hostHandoff.js +32 -55
  9. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  10. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  11. package/dist/audit/cli/nextStepHelpers.js +8 -8
  12. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  13. package/dist/audit/io/artifacts.d.ts +0 -28
  14. package/dist/audit/io/artifacts.d.ts.map +1 -1
  15. package/dist/audit/io/artifacts.js +1 -65
  16. package/dist/audit/io/artifacts.js.map +1 -1
  17. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  18. package/dist/audit/orchestrator/advance.js +12 -10
  19. package/dist/audit/orchestrator/advance.js.map +1 -1
  20. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  21. package/dist/audit/orchestrator/designReviewSnapshot.js +2 -5
  22. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  23. package/dist/audit/reporting/mergeFindings.d.ts.map +1 -1
  24. package/dist/audit/reporting/mergeFindings.js +1 -2
  25. package/dist/audit/reporting/mergeFindings.js.map +1 -1
  26. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +25 -10
  27. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +1 -1
  28. package/dist/remediate/contractPipeline/cyclicSeamResolution.js +35 -88
  29. package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +1 -1
  30. package/dist/remediate/contractPipeline/idRegistry.d.ts +10 -20
  31. package/dist/remediate/contractPipeline/idRegistry.d.ts.map +1 -1
  32. package/dist/remediate/contractPipeline/idRegistry.js +10 -24
  33. package/dist/remediate/contractPipeline/idRegistry.js.map +1 -1
  34. package/dist/remediate/contractPipeline/obligationKinds.d.ts +27 -0
  35. package/dist/remediate/contractPipeline/obligationKinds.d.ts.map +1 -0
  36. package/dist/remediate/contractPipeline/obligationKinds.js +18 -0
  37. package/dist/remediate/contractPipeline/obligationKinds.js.map +1 -0
  38. package/dist/remediate/dedup/crossLensDedup.d.ts +2 -3
  39. package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
  40. package/dist/remediate/dedup/crossLensDedup.js +2 -4
  41. package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
  42. package/dist/remediate/index.d.ts +38 -8
  43. package/dist/remediate/index.d.ts.map +1 -1
  44. package/dist/remediate/index.js +75 -27
  45. package/dist/remediate/index.js.map +1 -1
  46. package/dist/remediate/steps/contractPipeline.d.ts +6 -62
  47. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  48. package/dist/remediate/steps/contractPipeline.js +11 -80
  49. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  50. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  51. package/dist/remediate/steps/dispatch/hostHandoff.js +57 -48
  52. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  53. package/dist/remediate/steps/nextStep.d.ts +83 -2
  54. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  55. package/dist/remediate/steps/nextStep.js +79 -11
  56. package/dist/remediate/steps/nextStep.js.map +1 -1
  57. package/dist/remediate/steps/types.d.ts +1 -1
  58. package/dist/remediate/steps/types.d.ts.map +1 -1
  59. package/dist/remediate/steps/types.js.map +1 -1
  60. package/dist/remediate/utils/fileIntegrity.d.ts.map +1 -1
  61. package/dist/remediate/utils/fileIntegrity.js +123 -30
  62. package/dist/remediate/utils/fileIntegrity.js.map +1 -1
  63. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  64. package/dist/remediate/validation/artifacts.js +3 -9
  65. package/dist/remediate/validation/artifacts.js.map +1 -1
  66. package/dist/remediate/validation/contractPipeline.d.ts +1 -1
  67. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  68. package/dist/remediate/validation/contractPipeline.js +8 -46
  69. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  70. package/dist/remediate/validation/contractPipelineGates.d.ts +12 -20
  71. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  72. package/dist/remediate/validation/contractPipelineGates.js +107 -135
  73. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  74. package/dist/shared/engine/obligationEngine.d.ts +32 -0
  75. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  76. package/dist/shared/engine/obligationEngine.js +29 -0
  77. package/dist/shared/engine/obligationEngine.js.map +1 -1
  78. package/dist/shared/findings/dedupe.d.ts +4 -11
  79. package/dist/shared/findings/dedupe.d.ts.map +1 -1
  80. package/dist/shared/findings/dedupe.js +196 -163
  81. package/dist/shared/findings/dedupe.js.map +1 -1
  82. package/dist/shared/graph/directedCycles.d.ts +61 -0
  83. package/dist/shared/graph/directedCycles.d.ts.map +1 -0
  84. package/dist/shared/graph/directedCycles.js +190 -0
  85. package/dist/shared/graph/directedCycles.js.map +1 -0
  86. package/dist/shared/index.d.ts +6 -3
  87. package/dist/shared/index.d.ts.map +1 -1
  88. package/dist/shared/index.js +4 -2
  89. package/dist/shared/index.js.map +1 -1
  90. package/dist/shared/io/repoRoot.d.ts +45 -0
  91. package/dist/shared/io/repoRoot.d.ts.map +1 -1
  92. package/dist/shared/io/repoRoot.js +91 -1
  93. package/dist/shared/io/repoRoot.js.map +1 -1
  94. package/dist/shared/submission/submissionScan.d.ts +86 -0
  95. package/dist/shared/submission/submissionScan.d.ts.map +1 -0
  96. package/dist/shared/submission/submissionScan.js +93 -0
  97. package/dist/shared/submission/submissionScan.js.map +1 -0
  98. package/dist/shared/types/obligationLedger.d.ts +7 -3
  99. package/dist/shared/types/obligationLedger.d.ts.map +1 -1
  100. package/dist/shared/types/obligationLedger.js +9 -37
  101. package/dist/shared/types/obligationLedger.js.map +1 -1
  102. package/package.json +3 -2
  103. package/skills/audit-code/audit-code.prompt.md +5 -2
  104. package/wrapper/audit-code-wrapper-install-hosts.mjs +8 -3
  105. package/wrapper/audit-code-wrapper-lib.mjs +10 -3
  106. package/wrapper/installer-verb-help.mjs +3 -1
  107. package/wrapper/remediate-code-wrapper-install-hosts.mjs +8 -3
  108. package/wrapper/repo-root.mjs +80 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The ONE per-item host-submission scan both ingests draw from.
3
+ *
4
+ * Every submitted work item crosses the same four-step gate before any draw's
5
+ * domain work begins: resolve the bound path inside the boundary, read the
6
+ * bytes, classify a missing / unparseable / contract-invalid document, and
7
+ * refuse a `result_id` the caller has already seen. Both twins hand-rolled that
8
+ * sequence — audit in `readSubmittedResult` plus an inline duplicate check,
9
+ * remediate inline in its ingest loop — and the two copies had already drifted
10
+ * on CONTAINMENT: remediate re-checked the resolved path under the artifacts
11
+ * dir, audit checked the root only. This module resolves that in the stricter
12
+ * direction for BOTH draws.
13
+ *
14
+ * What it deliberately does NOT own — because these are the places the draws
15
+ * genuinely differ, not places they drifted:
16
+ *
17
+ * - the MESSAGE TEXT of each refusal (a per-draw renderer supplies it; the two
18
+ * vocabularies are test-pinned and address different readers);
19
+ * - the DOMAIN PARSER (a callback; each draw's parser stays standalone, so the
20
+ * hand-recovery lane can wrap `parseResult` directly and stay parity-pinned);
21
+ * - identity enforcement, which lives INSIDE those parsers — hoisting it would
22
+ * change refusal precedence for a doubly-bad document;
23
+ * - loop iteration, aggregation order, seen-set seeding, and the TIMING of the
24
+ * duplicate consume. Audit seeds from its persisted accepted ledger and
25
+ * consumes only after conversion, validation and grounding; remediate uses a
26
+ * fresh per-call set and consumes at parse. This scan only CHECKS.
27
+ */
28
+ import { type SubmissionIssue } from "./submissionClassifier.js";
29
+ /**
30
+ * The domain parser's answer, normalized. `detail` is handed straight to the
31
+ * draw's `contractInvalid` renderer — the scan never composes refusal prose of
32
+ * its own.
33
+ */
34
+ export type SubmissionParse<TParsed> = {
35
+ readonly ok: true;
36
+ readonly parsed: TParsed;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly detail: string;
40
+ };
41
+ /**
42
+ * The per-draw refusal vocabulary. Text is a DRAW concern: the audit ingest
43
+ * addresses a host repairing a bound result file, remediate addresses one
44
+ * repairing a pending work item, and several strings are pinned by tests.
45
+ */
46
+ export interface SubmissionScanMessages {
47
+ /** Nothing exists at the bound path. */
48
+ readonly missing: () => string;
49
+ /** Bytes exist but are not JSON; `detail` is the parse/IO reason. */
50
+ readonly malformed: (detail: string) => string;
51
+ /** JSON that the domain parser refused; `detail` is its stated reason. */
52
+ readonly contractInvalid: (detail: string) => string;
53
+ /** The parsed result re-uses an identity the caller has already seen. */
54
+ readonly duplicate: (resultId: string) => string;
55
+ }
56
+ /** Either the parsed submission, or the ONE issue that names why not. */
57
+ export type SubmissionScanOutcome<TParsed> = {
58
+ readonly ok: true;
59
+ readonly parsed: TParsed;
60
+ } | {
61
+ readonly ok: false;
62
+ readonly issue: SubmissionIssue;
63
+ };
64
+ /**
65
+ * Scan one bound submission. Throws only where the twins already threw: a bound
66
+ * path that escapes the repository root or the artifacts dir is a boundary
67
+ * violation, not a classified submission failure.
68
+ */
69
+ export declare function scanBoundSubmission<TParsed>(params: {
70
+ /** Absolute repository root the bound path is expressed relative to. */
71
+ readonly root: string;
72
+ /** Absolute artifacts dir the submission must also stay beneath. */
73
+ readonly artifactsDir: string;
74
+ /** Names the item in the containment refusal and in both issue locators. */
75
+ readonly workItemId: string;
76
+ /** Repository-relative bound path, as the persisted contract declares it. */
77
+ readonly resultPath: string;
78
+ /** The draw's standalone contract gate. */
79
+ readonly parse: (value: unknown) => SubmissionParse<TParsed>;
80
+ /** The submitted identity the duplicate check is asked about. */
81
+ readonly resultId: (parsed: TParsed) => string;
82
+ /** Caller-owned predicate; seeding and consume timing stay per-draw. */
83
+ readonly seen: (resultId: string) => boolean;
84
+ readonly messages: SubmissionScanMessages;
85
+ }): Promise<SubmissionScanOutcome<TParsed>>;
86
+ //# sourceMappingURL=submissionScan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissionScan.d.ts","sourceRoot":"","sources":["../../../src/shared/submission/submissionScan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,2BAA2B,CAAC;AAGnC;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,IAC/B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACrD,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CAClD;AAED,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,CAAC,OAAO,IACrC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;CAAE,CAAC;AAE5D;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE;IACzD,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,eAAe,CAAC,OAAO,CAAC,CAAC;IAC7D,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC;IAC/C,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;CAC3C,GAAG,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CA2D1C"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The ONE per-item host-submission scan both ingests draw from.
3
+ *
4
+ * Every submitted work item crosses the same four-step gate before any draw's
5
+ * domain work begins: resolve the bound path inside the boundary, read the
6
+ * bytes, classify a missing / unparseable / contract-invalid document, and
7
+ * refuse a `result_id` the caller has already seen. Both twins hand-rolled that
8
+ * sequence — audit in `readSubmittedResult` plus an inline duplicate check,
9
+ * remediate inline in its ingest loop — and the two copies had already drifted
10
+ * on CONTAINMENT: remediate re-checked the resolved path under the artifacts
11
+ * dir, audit checked the root only. This module resolves that in the stricter
12
+ * direction for BOTH draws.
13
+ *
14
+ * What it deliberately does NOT own — because these are the places the draws
15
+ * genuinely differ, not places they drifted:
16
+ *
17
+ * - the MESSAGE TEXT of each refusal (a per-draw renderer supplies it; the two
18
+ * vocabularies are test-pinned and address different readers);
19
+ * - the DOMAIN PARSER (a callback; each draw's parser stays standalone, so the
20
+ * hand-recovery lane can wrap `parseResult` directly and stay parity-pinned);
21
+ * - identity enforcement, which lives INSIDE those parsers — hoisting it would
22
+ * change refusal precedence for a doubly-bad document;
23
+ * - loop iteration, aggregation order, seen-set seeding, and the TIMING of the
24
+ * duplicate consume. Audit seeds from its persisted accepted ledger and
25
+ * consumes only after conversion, validation and grounding; remediate uses a
26
+ * fresh per-call set and consumes at parse. This scan only CHECKS.
27
+ */
28
+ // Sibling modules imported DIRECTLY, never through ../index.js: the barrel
29
+ // re-exports this module, so importing the barrel here would close a cycle
30
+ // (index → submissionScan → index) that check:depgraph refuses.
31
+ import { readSubmissionDocument, } from "./submissionClassifier.js";
32
+ import { resolveContainedPath } from "./submissionIdentity.js";
33
+ /**
34
+ * Scan one bound submission. Throws only where the twins already threw: a bound
35
+ * path that escapes the repository root or the artifacts dir is a boundary
36
+ * violation, not a classified submission failure.
37
+ */
38
+ export async function scanBoundSubmission(params) {
39
+ const locators = {
40
+ work_item_id: params.workItemId,
41
+ result_path: params.resultPath,
42
+ };
43
+ const label = `result path for ${params.workItemId}`;
44
+ const absolutePath = resolveContainedPath(params.root, params.resultPath, label);
45
+ // Both boundaries, both draws: a submission lives under the artifacts dir,
46
+ // and root containment alone would admit any other tracked file in the repo.
47
+ resolveContainedPath(params.artifactsDir, absolutePath, label);
48
+ const read = await readSubmissionDocument(absolutePath);
49
+ if (read.kind === "missing") {
50
+ return {
51
+ ok: false,
52
+ issue: {
53
+ code: "submission_missing",
54
+ message: params.messages.missing(),
55
+ ...locators,
56
+ },
57
+ };
58
+ }
59
+ if (read.kind === "malformed") {
60
+ return {
61
+ ok: false,
62
+ issue: {
63
+ code: "submission_malformed",
64
+ message: params.messages.malformed(read.detail),
65
+ ...locators,
66
+ },
67
+ };
68
+ }
69
+ const parsed = params.parse(read.value);
70
+ if (!parsed.ok) {
71
+ return {
72
+ ok: false,
73
+ issue: {
74
+ code: "submission_contract_invalid",
75
+ message: params.messages.contractInvalid(parsed.detail),
76
+ ...locators,
77
+ },
78
+ };
79
+ }
80
+ const resultId = params.resultId(parsed.parsed);
81
+ if (params.seen(resultId)) {
82
+ return {
83
+ ok: false,
84
+ issue: {
85
+ code: "duplicate_submission_id",
86
+ message: params.messages.duplicate(resultId),
87
+ ...locators,
88
+ },
89
+ };
90
+ }
91
+ return { ok: true, parsed: parsed.parsed };
92
+ }
93
+ //# sourceMappingURL=submissionScan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissionScan.js","sourceRoot":"","sources":["../../../src/shared/submission/submissionScan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,2EAA2E;AAC3E,2EAA2E;AAC3E,gEAAgE;AAChE,OAAO,EACL,sBAAsB,GAEvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAgC/D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAU,MAgBlD;IACC,MAAM,QAAQ,GAAG;QACf,YAAY,EAAE,MAAM,CAAC,UAAU;QAC/B,WAAW,EAAE,MAAM,CAAC,UAAU;KACtB,CAAC;IACX,MAAM,KAAK,GAAG,mBAAmB,MAAM,CAAC,UAAU,EAAE,CAAC;IACrD,MAAM,YAAY,GAAG,oBAAoB,CACvC,MAAM,CAAC,IAAI,EACX,MAAM,CAAC,UAAU,EACjB,KAAK,CACN,CAAC;IACF,2EAA2E;IAC3E,6EAA6E;IAC7E,oBAAoB,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAG,MAAM,sBAAsB,CAAC,YAAY,CAAC,CAAC;IACxD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE;gBAClC,GAAG,QAAQ;aACZ;SACF,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,sBAAsB;gBAC5B,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC/C,GAAG,QAAQ;aACZ;SACF,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,6BAA6B;gBACnC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;gBACvD,GAAG,QAAQ;aACZ;SACF,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;gBAC5C,GAAG,QAAQ;aACZ;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AAC7C,CAAC"}
@@ -8,9 +8,13 @@
8
8
  */
9
9
  import type { ObligationEntry, ObligationLedger } from "./contractPipeline.js";
10
10
  /**
11
- * Detect cycles in an obligation dependency graph using DFS with three-color
12
- * marking (white/gray/black). Returns the IDs of obligations forming the first
13
- * cycle detected, or null when the graph is acyclic.
11
+ * Detect cycles in an obligation dependency graph. Returns the IDs of
12
+ * obligations forming the first cycle detected as a witness path with its start
13
+ * repeated at the end (`["A","B","A"]`), or null when the graph is acyclic.
14
+ *
15
+ * A thin draw over the shared directed-cycle core: an obligation that depends
16
+ * on ITSELF is a cycle here (self-loops included), and dependencies naming an
17
+ * id outside the ledger are external references the core ignores.
14
18
  *
15
19
  * INV-shared-core-07: called at construction time so a cycle is caught early
16
20
  * rather than causing an infinite loop or confusing error at scheduling time.
@@ -1 +1 @@
1
- {"version":3,"file":"obligationLedger.d.ts","sourceRoot":"","sources":["../../../src/shared/types/obligationLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAK/B;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,SAAS,eAAe,EAAE,GACtC,MAAM,EAAE,GAAG,IAAI,CAmCjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,gBAAgB,CAgBlB"}
1
+ {"version":3,"file":"obligationLedger.d.ts","sourceRoot":"","sources":["../../../src/shared/types/obligationLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAM/B;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,SAAS,eAAe,EAAE,GACtC,MAAM,EAAE,GAAG,IAAI,CAEjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,gBAAgB,CAgBlB"}
@@ -7,49 +7,21 @@
7
7
  * if a dependency cycle is present.
8
8
  */
9
9
  import { CONTRACT_PIPELINE_OBLIGATION_LEDGER_VERSION, } from "./contractPipeline.js";
10
+ import { findFirstCycleWitness } from "../graph/directedCycles.js";
10
11
  /**
11
- * Detect cycles in an obligation dependency graph using DFS with three-color
12
- * marking (white/gray/black). Returns the IDs of obligations forming the first
13
- * cycle detected, or null when the graph is acyclic.
12
+ * Detect cycles in an obligation dependency graph. Returns the IDs of
13
+ * obligations forming the first cycle detected as a witness path with its start
14
+ * repeated at the end (`["A","B","A"]`), or null when the graph is acyclic.
15
+ *
16
+ * A thin draw over the shared directed-cycle core: an obligation that depends
17
+ * on ITSELF is a cycle here (self-loops included), and dependencies naming an
18
+ * id outside the ledger are external references the core ignores.
14
19
  *
15
20
  * INV-shared-core-07: called at construction time so a cycle is caught early
16
21
  * rather than causing an infinite loop or confusing error at scheduling time.
17
22
  */
18
23
  export function detectObligationCycle(obligations) {
19
- const idSet = new Set(obligations.map((o) => o.id));
20
- const depMap = new Map();
21
- for (const o of obligations) {
22
- depMap.set(o.id, o.depends_on.filter((dep) => idSet.has(dep)));
23
- }
24
- // Three-color DFS: 0 = unvisited, 1 = in-stack (gray), 2 = done (black).
25
- const color = new Map();
26
- const path = [];
27
- function dfs(id) {
28
- const c = color.get(id) ?? 0;
29
- if (c === 2)
30
- return null; // already done
31
- if (c === 1) {
32
- // Cycle detected: return the cycle slice.
33
- const cycleStart = path.indexOf(id);
34
- return path.slice(cycleStart).concat(id);
35
- }
36
- color.set(id, 1);
37
- path.push(id);
38
- for (const dep of depMap.get(id) ?? []) {
39
- const cycle = dfs(dep);
40
- if (cycle)
41
- return cycle;
42
- }
43
- path.pop();
44
- color.set(id, 2);
45
- return null;
46
- }
47
- for (const o of obligations) {
48
- const cycle = dfs(o.id);
49
- if (cycle)
50
- return cycle;
51
- }
52
- return null;
24
+ return findFirstCycleWitness(obligations, { includeSelfLoops: true });
53
25
  }
54
26
  /**
55
27
  * Build a validated ObligationLedger.
@@ -1 +1 @@
1
- {"version":3,"file":"obligationLedger.js","sourceRoot":"","sources":["../../../src/shared/types/obligationLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EACL,2CAA2C,GAC5C,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAuC;IAEvC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,yEAAyE;IACzE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,SAAS,GAAG,CAAC,EAAU;QACrB,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,eAAe;QACzC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,0CAA0C;YAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACpC,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACd,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,GAAG,EAAE,CAAC;QACX,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxB,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAQD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAqC;IAErC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAErD,MAAM,KAAK,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,wFAAwF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC5G,CAAC;IACJ,CAAC;IAED,OAAO;QACL,gBAAgB,EAAE,2CAA2C;QAC7D,OAAO;QACP,WAAW;QACX,UAAU,EAAE,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACnD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"obligationLedger.js","sourceRoot":"","sources":["../../../src/shared/types/obligationLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EACL,2CAA2C,GAC5C,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAuC;IAEvC,OAAO,qBAAqB,CAAC,WAAW,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC;AACxE,CAAC;AAQD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAqC;IAErC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAErD,MAAM,KAAK,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,wFAAwF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC5G,CAAC;IACJ,CAAC;IAED,OAAO;QACL,gBAAgB,EAAE,2CAA2C;QAC7D,OAAO;QACP,WAAW;QACX,UAAU,EAAE,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACnD,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-tools",
3
- "version": "0.49.0",
3
+ "version": "0.50.2",
4
4
  "private": false,
5
5
  "license": "ISC",
6
6
  "description": "Provider-neutral code auditing and remediation workflows for arbitrary repositories.",
@@ -51,7 +51,7 @@
51
51
  "test": "npm run build && node scripts/shared/run-vitest-gate.mjs",
52
52
  "test:single": "npm run build && node scripts/shared/run-vitest-gate.mjs",
53
53
  "verify:guards": "node scripts/shared/run-vitest-gate.mjs --retry=2 --exclude \"**/audit-code-completion-*.test.ts\" --exclude \"**/audit-code-wrapper-*.test.ts\" --exclude \"**/audit/next-step-core-*.test.ts\" --exclude \"**/seam-host-only-next-step.test.ts\" --exclude \"**/*-e2e.test.*\" --exclude \"**/next-step-implement-dispatch.test.ts\" --exclude \"**/next-step-pipeline-dispatch.test.ts\"",
54
- "verify:checks": "node scripts/shared/profile-run.mjs verify-checks check:control-bytes check:shared-primitives check:version-gates check:guard-reach check:ci-trigger-paths check:offload-lanes check:loop-core-patterns check:constitutional-doc-paths check:runtime-artifact-names check:executor-producers check:cli-surface check:deadcode check:lint check:dup check:depgraph check:doc-manifest check:doc-links check:doc-code-citations check:gate-enumeration check:philosophy-brief check:readme-sample-report check:proposal-red-at check:nightly-routine-prompt check:handoff-roadmap check:backlog-index check:memory-citations check:backlog-budget check:backlog-status check:backlog-line-numbers check:tests build check:scripts verify:hosts verify:remediate-hosts pack:smoke smoke:packaged-audit-code smoke:packaged-remediate-code",
54
+ "verify:checks": "node scripts/shared/profile-run.mjs verify-checks check:control-bytes check:shared-primitives check:version-gates check:guard-reach check:ci-trigger-paths check:offload-lanes check:loop-core-patterns check:constitutional-doc-paths check:runtime-artifact-names check:executor-producers check:spec-mirrors check:cli-surface check:deadcode check:lint check:dup check:depgraph check:doc-manifest check:doc-links check:doc-code-citations check:gate-enumeration check:philosophy-brief check:readme-sample-report check:proposal-red-at check:nightly-routine-prompt check:handoff-roadmap check:backlog-index check:memory-citations check:backlog-budget check:backlog-status check:backlog-line-numbers check:tests build check:scripts verify:hosts verify:remediate-hosts pack:smoke smoke:packaged-audit-code smoke:packaged-remediate-code",
55
55
  "verify:release": "npm run verify:checks && node scripts/shared/run-vitest-gate.mjs && npm run smoke:linked-audit-code && npm run smoke:linked-remediate-code",
56
56
  "check:control-bytes": "node scripts/check-control-bytes.mjs",
57
57
  "check:shared-primitives": "node scripts/check-shared-primitives.mjs",
@@ -101,6 +101,7 @@
101
101
  "check:backlog-line-numbers": "node scripts/check-backlog-line-numbers.mjs",
102
102
  "check:runtime-artifact-names": "node scripts/shared/generate-runtime-artifact-names.mjs --check",
103
103
  "check:executor-producers": "node scripts/shared/generate-executor-producers.mjs --check",
104
+ "check:spec-mirrors": "node scripts/shared/generate-spec-mirrors.mjs --check",
104
105
  "check:cli-surface": "node scripts/shared/generate-cli-surface.mjs --check",
105
106
  "generate:shard-baseline": "node scripts/shared/generate-vitest-shard-baseline.mjs"
106
107
  },
@@ -21,8 +21,11 @@ repository root.
21
21
 
22
22
  Preserve user arguments:
23
23
 
24
- - pass the user-supplied target directory with `--root <path>` on every
25
- `audit-code` command (`ensure` and each `next-step`)
24
+ - run `audit-code` from inside the target repository; every command resolves
25
+ that repository's root from the working directory on its own, so normal usage
26
+ passes no `--root`
27
+ - pass the user-supplied target directory with `--root <path>` only when running
28
+ from outside that repository
26
29
 
27
30
  This is the one full statement of the target-directory rule; the `audit-code`
28
31
  skill points here instead of restating it.
@@ -19,6 +19,7 @@ import {
19
19
  findLegacyAuditCodeSurfaceFiles,
20
20
  removeLegacyAuditCodeSurfaceFiles,
21
21
  } from './audit-code-wrapper-legacy.mjs';
22
+ import { resolveWrapperRoot } from './repo-root.mjs';
22
23
  import {
23
24
  renderVSCodeAgentFile,
24
25
  renderCodexAutomationRecipe,
@@ -603,7 +604,11 @@ export async function writeAntigravityAssets(assetPaths, promptBody, skillSource
603
604
 
604
605
  export async function installBootstrap(argv, options = {}) {
605
606
  const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
606
- const root = resolve(getFlag(argv, '--root') ?? '.');
607
+ // An absent --root is the repository the caller is standing in, discovered
608
+ // the same way `next-step` discovers it — never the cwd verbatim, which would
609
+ // deposit a full set of host assets in whatever subdirectory the operator
610
+ // happened to be in.
611
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
607
612
  await assertDirectoryExists(root, 'Target repository root');
608
613
  const profile = getInstallProfile(host);
609
614
  const promptSource = await readFile(promptAssetPath, 'utf8');
@@ -828,7 +833,7 @@ export async function verifyHostsIsolated(options = {}) {
828
833
  }
829
834
 
830
835
  export async function verifyInstalledBootstrap(argv) {
831
- const root = resolve(getFlag(argv, '--root') ?? '.');
836
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
832
837
  const requestedHost = getFlag(argv, '--host')?.toLowerCase() ?? null;
833
838
  const installManifestPath = join(
834
839
  root,
@@ -1118,7 +1123,7 @@ export async function detectBootstrapRefreshReason(root, host) {
1118
1123
 
1119
1124
  export async function ensureBootstrap(argv) {
1120
1125
  const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
1121
- const root = resolve(getFlag(argv, '--root') ?? '.');
1126
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
1122
1127
  const quiet = hasFlag(argv, '--quiet');
1123
1128
  const force = hasFlag(argv, '--force');
1124
1129
  await assertDirectoryExists(root, 'Target repository root');
@@ -20,6 +20,7 @@ import {
20
20
  installerVerbHelp,
21
21
  installerVerbSummaries,
22
22
  } from './installer-verb-help.mjs';
23
+ import { resolveWrapperRoot } from './repo-root.mjs';
23
24
 
24
25
  export { shouldBuildDistForPaths, assertWorkspaceInstalled };
25
26
  export { _INSTALL_HOST_ORDER, _INSTALL_HOST_DEFINITIONS, _getInstallHostKeys, _getInstallProfile, _renderGeminiCommandToml };
@@ -263,7 +264,7 @@ function printHelp({ usageName, preferredEntrypoint }) {
263
264
  '- status summarizes deterministic audit state and pending review work',
264
265
  '',
265
266
  'Defaults:',
266
- '- --root .',
267
+ '- --root the repository the working directory is in (nearest ancestor owning .audit-tools/ or .git); pass it only to target a repository you are not inside',
267
268
  '- --artifacts-dir <root>/.audit-tools/audit',
268
269
  ];
269
270
 
@@ -284,7 +285,11 @@ async function printPromptPath() {
284
285
 
285
286
  async function runDistCommand(commandName, argv, { ensureArtifactsDir = false } = {}) {
286
287
  const commandArgs = [...argv];
287
- const rootValue = resolve(getFlag(commandArgs, '--root') ?? '.');
288
+ // An absent --root is DISCOVERED (nearest ancestor owning .audit-tools/.git),
289
+ // not defaulted to the caller's cwd verbatim: the wrapper normalizes the root
290
+ // to an absolute path before the child sees it (CE-001), so a cwd-verbatim
291
+ // default here would pin the wrong root no matter what dist resolves.
292
+ const rootValue = resolveWrapperRoot(getFlag(commandArgs, '--root'));
288
293
  const artifactsDir = resolve(getFlag(commandArgs, '--artifacts-dir') ?? join(rootValue, '.audit-tools', 'audit'));
289
294
 
290
295
  // Overwrite (not default) so a user-supplied relative value is normalized to
@@ -304,7 +309,9 @@ async function runDistCommand(commandName, argv, { ensureArtifactsDir = false }
304
309
 
305
310
  async function runDistCommandInline(commandName, argv, { ensureArtifactsDir = false } = {}) {
306
311
  const commandArgs = [...argv];
307
- const rootValue = resolve(getFlag(commandArgs, '--root') ?? '.');
312
+ // Same discovery as runDistCommand the two paths must not answer "which
313
+ // repository is this?" differently.
314
+ const rootValue = resolveWrapperRoot(getFlag(commandArgs, '--root'));
308
315
  const artifactsDir = resolve(getFlag(commandArgs, '--artifacts-dir') ?? join(rootValue, '.audit-tools', 'audit'));
309
316
 
310
317
  setFlag(commandArgs, '--root', rootValue);
@@ -97,7 +97,9 @@ export function installerVerbHelp(verb, { usageName, product }) {
97
97
  ...VERB_DETAIL[verb],
98
98
  '',
99
99
  'Common options:',
100
- '- --root PATH repository root (default: .)',
100
+ // NOT "(default: .)" — an absent --root is DISCOVERED (nearest ancestor
101
+ // owning .audit-tools/ or .git), not the cwd verbatim, on both bins.
102
+ '- --root PATH repository root (default: the repository the working directory is in)',
101
103
  '- --quiet suppress output',
102
104
  ].join('\n');
103
105
  }
@@ -21,6 +21,7 @@ import {
21
21
  findLegacyRemediateCodeSurfaceFiles,
22
22
  removeLegacyRemediateCodeSurfaceFiles,
23
23
  } from './remediate-code-wrapper-legacy.mjs';
24
+ import { resolveWrapperRoot } from './repo-root.mjs';
24
25
  import {
25
26
  renderVSCodeAgentFile,
26
27
  renderCodexAutomationRecipe,
@@ -597,7 +598,11 @@ export async function writeAntigravityAssets(assetPaths, promptBody, skillSource
597
598
 
598
599
  export async function installBootstrap(argv, options = {}) {
599
600
  const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
600
- const root = resolve(getFlag(argv, '--root') ?? '.');
601
+ // An absent --root is the repository the caller is standing in, discovered
602
+ // the same way `next-step` discovers it — never the cwd verbatim, which would
603
+ // deposit a full set of host assets in whatever subdirectory the operator
604
+ // happened to be in.
605
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
601
606
  await assertDirectoryExists(root, 'Target repository root');
602
607
  const profile = getInstallProfile(host);
603
608
  const promptSource = (await readFile(promptAssetPath, 'utf8')).replace(/\r\n/g, '\n');
@@ -864,7 +869,7 @@ export async function verifyHostsIsolated(options = {}) {
864
869
  }
865
870
 
866
871
  export async function verifyInstalledBootstrap(argv) {
867
- const root = resolve(getFlag(argv, '--root') ?? '.');
872
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
868
873
  const requestedHost = getFlag(argv, '--host')?.toLowerCase() ?? null;
869
874
  const installManifestPath = join(
870
875
  root,
@@ -1154,7 +1159,7 @@ export async function detectBootstrapRefreshReason(root, host) {
1154
1159
 
1155
1160
  export async function ensureBootstrap(argv) {
1156
1161
  const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
1157
- const root = resolve(getFlag(argv, '--root') ?? '.');
1162
+ const root = resolveWrapperRoot(getFlag(argv, '--root'));
1158
1163
  const quiet = hasFlag(argv, '--quiet');
1159
1164
  const force = hasFlag(argv, '--force');
1160
1165
  await assertDirectoryExists(root, 'Target repository root');
@@ -0,0 +1,80 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve, sep } from 'node:path';
4
+
5
+ // Canonical repo-root discovery for the WRAPPER side of BOTH bins.
6
+ //
7
+ // Bin-neutral by name and by ownership, like `installer-verb-help.mjs`: audit-code
8
+ // and remediate-code answer their installer verbs from this one module, so the two
9
+ // wrappers cannot resolve "no --root" differently (one core, two draws — never a
10
+ // per-bin fork of the same algorithm).
11
+ //
12
+ // This is a deliberate, pinned MIRROR of `discoverRepoRoot` / `climbOutOfAuditTools`
13
+ // in src/shared/io/repoRoot.ts, kept for the same bootstrap constraint that
14
+ // forces `quoteForCmd` to be mirrored here: the wrapper runs BEFORE dist exists
15
+ // (the installer verbs `ensure` / `install` / `verify-install` are answered
16
+ // entirely in the wrapper and must never trigger a build just to learn where
17
+ // they are), so it cannot import the TypeScript source or the built module.
18
+ // `tests/shared/wrapper-repo-root-parity.test.ts` runs BOTH implementations over
19
+ // the same fixture trees and fails if they ever disagree — the two copies cannot
20
+ // drift silently.
21
+ //
22
+ // Behaviour, stated once here and pinned by that parity test: climb out of any
23
+ // `.audit-tools/` the start dir sits inside, then walk up to the nearest
24
+ // ancestor carrying `.audit-tools` or `.git` (either may be a file — a linked
25
+ // worktree's `.git` is one), stopping BELOW the home directory because
26
+ // `~/.audit-tools/` is the tool's machine-wide cache home and would otherwise
27
+ // match on every box. No marker → the start dir unchanged.
28
+
29
+ export const AUDIT_TOOLS_DIRNAME = '.audit-tools';
30
+
31
+ export const REPO_ROOT_MARKERS = [AUDIT_TOOLS_DIRNAME, '.git'];
32
+
33
+ /** @param {string} p */
34
+ export function climbOutOfAuditTools(p) {
35
+ const resolved = resolve(p);
36
+ const segments = resolved.split(sep);
37
+ const idx = segments.indexOf(AUDIT_TOOLS_DIRNAME);
38
+ if (idx <= 0) return resolved;
39
+ return segments.slice(0, idx).join(sep) || resolved;
40
+ }
41
+
42
+ /** @returns {string | null} */
43
+ function repoRootCeiling() {
44
+ try {
45
+ const home = homedir();
46
+ return home ? resolve(home) : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /** @param {string} [startDir] @returns {string} */
53
+ export function discoverRepoRoot(startDir = process.cwd()) {
54
+ const start = climbOutOfAuditTools(resolve(startDir));
55
+ const ceiling = repoRootCeiling();
56
+ let current = start;
57
+ for (;;) {
58
+ if (ceiling !== null && current === ceiling) return start;
59
+ if (REPO_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))) {
60
+ return current;
61
+ }
62
+ const parent = dirname(current);
63
+ if (parent === current) return start;
64
+ current = parent;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The root a wrapper-side command acts on: an explicitly supplied `--root` is
70
+ * honored verbatim (resolved against the caller's cwd), an absent one is
71
+ * discovered. The ONE place either wrapper turns "no --root" into a directory,
72
+ * so `next-step`, `ensure`, `install` and `verify-install` cannot answer it
73
+ * differently — across both bins.
74
+ *
75
+ * @param {string | undefined} rawRoot
76
+ * @returns {string}
77
+ */
78
+ export function resolveWrapperRoot(rawRoot) {
79
+ return rawRoot === undefined ? discoverRepoRoot() : resolve(rawRoot);
80
+ }