@ecoma-io/archkeep 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/cli.mjs +571 -61
- package/commands.mjs +57 -0
- package/lsp.mjs +15 -2
- package/package.json +8 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +11 -3
- package/src/commands/README.md +52 -1
- package/src/commands/change-intent.mjs +461 -0
- package/src/commands/change.mjs +612 -0
- package/src/commands/check.mjs +2 -1
- package/src/commands/context.mjs +124 -16
- package/src/commands/custom-rules.mjs +286 -2
- package/src/commands/delta-classify.mjs +195 -33
- package/src/commands/delta-snapshot.mjs +156 -1
- package/src/commands/delta.mjs +142 -17
- package/src/commands/diff.mjs +41 -13
- package/src/commands/evolution.mjs +473 -0
- package/src/commands/history.mjs +130 -103
- package/src/commands/policy.mjs +57 -0
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/commands/trajectory.mjs +437 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/path-util.mjs +40 -0
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/report/change-text.mjs +148 -0
- package/src/report/delta-text.mjs +82 -1
- package/src/report/evolution-text.mjs +83 -0
- package/src/report/history-text.mjs +4 -114
- package/src/report/sarif.mjs +255 -0
- package/src/report/snapshot-text.mjs +123 -0
- package/src/report/trajectory-text.mjs +143 -0
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/tsconfig-paths.mjs +3 -2
- package/src/workspace.mjs +115 -23
package/src/commands/history.mjs
CHANGED
|
@@ -251,6 +251,135 @@ export function nextSequence(read) {
|
|
|
251
251
|
return String(max + 1).padStart(width, "0");
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Classifies ONE transition — the record `computeEvolution` pushes for the
|
|
256
|
+
* consecutive pair `(from, to)` — together with the raw metadata comparison it
|
|
257
|
+
* was decided over. Split out of `computeEvolution` so a second consumer
|
|
258
|
+
* (`./trajectory.mjs`) aggregates the SAME classification instead of growing a
|
|
259
|
+
* second copy of it: the signals, the disclosure notes and the three-state
|
|
260
|
+
* metadata facts are decided here once (`../README.md`'s single-home rule),
|
|
261
|
+
* and both commands read them from this one place.
|
|
262
|
+
*
|
|
263
|
+
* The returned `record` is exactly what lands in `history`'s envelope. The
|
|
264
|
+
* returned `meta` is the untouched `compareSnapshotMetadata` result — the
|
|
265
|
+
* per-side facts (`policyOneSided`, `provenanceOneSided`, `crossRepo`,
|
|
266
|
+
* `dirtyBaseline`, `dirtyHead`) that the record's `notes[]` render as prose.
|
|
267
|
+
* An aggregator that needs to COUNT those facts rather than print them reads
|
|
268
|
+
* `meta`; parsing `notes` strings would be a second copy of the decision
|
|
269
|
+
* wearing a parser's name.
|
|
270
|
+
*
|
|
271
|
+
* @param {{name: string, path: string, envelope: object, id: string}} from
|
|
272
|
+
* @param {{name: string, path: string, envelope: object, id: string}} to
|
|
273
|
+
* @returns {{record: {from: string, to: string, architectureChanged: boolean,
|
|
274
|
+
* changes: object|null, policyChanged: boolean|null, providerChanged: boolean,
|
|
275
|
+
* codeDrift: boolean, notes: string[]},
|
|
276
|
+
* meta: object}} `meta` is `compareSnapshotMetadata`'s result.
|
|
277
|
+
*/
|
|
278
|
+
export function classifyTransition(from, to) {
|
|
279
|
+
const meta = compareSnapshotMetadata({
|
|
280
|
+
baselineProvider: from.envelope.workspace.provider,
|
|
281
|
+
headProvider: to.envelope.workspace.provider,
|
|
282
|
+
baselineProvenance: from.envelope.workspace.provenance,
|
|
283
|
+
headProvenance: to.envelope.workspace.provenance,
|
|
284
|
+
baselineFingerprint: from.envelope.result.policy?.fingerprint ?? null,
|
|
285
|
+
headFingerprint: to.envelope.result.policy?.fingerprint ?? null,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const notes = [];
|
|
289
|
+
if (meta.policyChanged === true) {
|
|
290
|
+
// A policy change is disclosed the way `diff` discloses it — a fact
|
|
291
|
+
// about how the transition must be interpreted, not a structural
|
|
292
|
+
// change and not a refusal.
|
|
293
|
+
notes.push(
|
|
294
|
+
"policy (the declared architectural intent) changed between these snapshots — " +
|
|
295
|
+
"the boundary law differs even though the graph may not",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (meta.providerChanged) {
|
|
299
|
+
notes.push(
|
|
300
|
+
`provider changed (${from.envelope.workspace.provider} → ${to.envelope.workspace.provider}) — ` +
|
|
301
|
+
"structural differences may be provider-artefacts rather than real architectural changes",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (meta.crossRepo) {
|
|
305
|
+
notes.push("provenance remotes differ — these snapshots may be from unrelated repositories");
|
|
306
|
+
}
|
|
307
|
+
// The one-sided cases are the silent direction: a fingerprint or
|
|
308
|
+
// provenance on one snapshot and not the other cannot be asserted "the
|
|
309
|
+
// same", so it is disclosed rather than read as unchanged.
|
|
310
|
+
if (meta.policyOneSided) {
|
|
311
|
+
notes.push(
|
|
312
|
+
"policy (the declared architectural intent) could not be compared — one snapshot " +
|
|
313
|
+
"records the boundary law and the other does not",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (meta.provenanceOneSided) {
|
|
317
|
+
notes.push(
|
|
318
|
+
"repository provenance could not be compared — one snapshot records its origin and the other does not",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
// A snapshot taken from a dirty tree is not a reproducible claim about the
|
|
322
|
+
// commit it names, so the transition says which side came from one rather
|
|
323
|
+
// than reading it as a claim about committed history.
|
|
324
|
+
if (meta.dirtyBaseline) {
|
|
325
|
+
const commit = from.envelope.workspace.provenance?.commit;
|
|
326
|
+
notes.push(
|
|
327
|
+
"the baseline snapshot was captured from an uncommitted (dirty) tree — its architecture " +
|
|
328
|
+
`is a claim about uncommitted state${typeof commit === "string" ? `, not about commit '${commit}'` : ""}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (meta.dirtyHead) {
|
|
332
|
+
const commit = to.envelope.workspace.provenance?.commit;
|
|
333
|
+
notes.push(
|
|
334
|
+
"the head snapshot was captured from an uncommitted (dirty) tree — its architecture " +
|
|
335
|
+
`is a claim about uncommitted state${typeof commit === "string" ? `, not about commit '${commit}'` : ""}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const diff = computeDiff(
|
|
340
|
+
{
|
|
341
|
+
projects: from.envelope.result.projects,
|
|
342
|
+
dependencies: from.envelope.result.dependencies,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
projects: to.envelope.result.projects,
|
|
346
|
+
dependencies: to.envelope.result.dependencies,
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
const architectureChanged =
|
|
350
|
+
diff.addedProjects.length > 0 ||
|
|
351
|
+
diff.removedProjects.length > 0 ||
|
|
352
|
+
diff.changedProjects.length > 0 ||
|
|
353
|
+
diff.addedEdges.length > 0 ||
|
|
354
|
+
diff.removedEdges.length > 0;
|
|
355
|
+
|
|
356
|
+
// Code drift is a disclosure, so it is only asserted when every signal
|
|
357
|
+
// that could refute it is verifiable and unchanged: the architecture did
|
|
358
|
+
// not move, the policy was actually compared and did not change, and
|
|
359
|
+
// provenance advanced. A `null` policyChanged (one-sided, or neither
|
|
360
|
+
// snapshot carries a fingerprint) is "could not be compared", not "the
|
|
361
|
+
// same" — asserting code drift on an unverifiable policy would report a
|
|
362
|
+
// clean transition where the tool cannot look.
|
|
363
|
+
const codeDrift =
|
|
364
|
+
!architectureChanged && meta.policyChanged === false && meta.provenanceChanged === true;
|
|
365
|
+
|
|
366
|
+
const record = {
|
|
367
|
+
from: from.name,
|
|
368
|
+
to: to.name,
|
|
369
|
+
architectureChanged,
|
|
370
|
+
// A provider change is rendered with an empty diff (no graph change on
|
|
371
|
+
// top of a carrier change), a policy-only transition with null — so a
|
|
372
|
+
// consumer can tell "the carrier changed" from "only the record's
|
|
373
|
+
// interpretation changed".
|
|
374
|
+
changes: architectureChanged || meta.providerChanged ? diff : null,
|
|
375
|
+
policyChanged: meta.policyChanged,
|
|
376
|
+
providerChanged: meta.providerChanged,
|
|
377
|
+
codeDrift,
|
|
378
|
+
notes,
|
|
379
|
+
};
|
|
380
|
+
return { record, meta };
|
|
381
|
+
}
|
|
382
|
+
|
|
254
383
|
/**
|
|
255
384
|
* Computes the evolution record from a list of snapshots: history order,
|
|
256
385
|
* each snapshot's identity, and the classified transition from each to the
|
|
@@ -274,109 +403,7 @@ export function computeEvolution(files) {
|
|
|
274
403
|
const transitions = [];
|
|
275
404
|
|
|
276
405
|
for (let i = 0; i + 1 < files.length; i++) {
|
|
277
|
-
|
|
278
|
-
const to = files[i + 1];
|
|
279
|
-
const meta = compareSnapshotMetadata({
|
|
280
|
-
baselineProvider: from.envelope.workspace.provider,
|
|
281
|
-
headProvider: to.envelope.workspace.provider,
|
|
282
|
-
baselineProvenance: from.envelope.workspace.provenance,
|
|
283
|
-
headProvenance: to.envelope.workspace.provenance,
|
|
284
|
-
baselineFingerprint: from.envelope.result.policy?.fingerprint ?? null,
|
|
285
|
-
headFingerprint: to.envelope.result.policy?.fingerprint ?? null,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const notes = [];
|
|
289
|
-
if (meta.policyChanged === true) {
|
|
290
|
-
// A policy change is disclosed the way `diff` discloses it — a fact
|
|
291
|
-
// about how the transition must be interpreted, not a structural
|
|
292
|
-
// change and not a refusal.
|
|
293
|
-
notes.push(
|
|
294
|
-
"policy (the declared architectural intent) changed between these snapshots — " +
|
|
295
|
-
"the boundary law differs even though the graph may not",
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
if (meta.providerChanged) {
|
|
299
|
-
notes.push(
|
|
300
|
-
`provider changed (${from.envelope.workspace.provider} → ${to.envelope.workspace.provider}) — ` +
|
|
301
|
-
"structural differences may be provider-artefacts rather than real architectural changes",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
if (meta.crossRepo) {
|
|
305
|
-
notes.push("provenance remotes differ — these snapshots may be from unrelated repositories");
|
|
306
|
-
}
|
|
307
|
-
// The one-sided cases are the silent direction: a fingerprint or
|
|
308
|
-
// provenance on one snapshot and not the other cannot be asserted "the
|
|
309
|
-
// same", so it is disclosed rather than read as unchanged.
|
|
310
|
-
if (meta.policyOneSided) {
|
|
311
|
-
notes.push(
|
|
312
|
-
"policy (the declared architectural intent) could not be compared — one snapshot " +
|
|
313
|
-
"records the boundary law and the other does not",
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
if (meta.provenanceOneSided) {
|
|
317
|
-
notes.push(
|
|
318
|
-
"repository provenance could not be compared — one snapshot records its origin and the other does not",
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
// A snapshot taken from a dirty tree is not a reproducible claim about the
|
|
322
|
-
// commit it names, so the transition says which side came from one rather
|
|
323
|
-
// than reading it as a claim about committed history.
|
|
324
|
-
if (meta.dirtyBaseline) {
|
|
325
|
-
const commit = from.envelope.workspace.provenance?.commit;
|
|
326
|
-
notes.push(
|
|
327
|
-
"the baseline snapshot was captured from an uncommitted (dirty) tree — its architecture " +
|
|
328
|
-
`is a claim about uncommitted state${typeof commit === "string" ? `, not about commit '${commit}'` : ""}`,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
if (meta.dirtyHead) {
|
|
332
|
-
const commit = to.envelope.workspace.provenance?.commit;
|
|
333
|
-
notes.push(
|
|
334
|
-
"the head snapshot was captured from an uncommitted (dirty) tree — its architecture " +
|
|
335
|
-
`is a claim about uncommitted state${typeof commit === "string" ? `, not about commit '${commit}'` : ""}`,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const diff = computeDiff(
|
|
340
|
-
{
|
|
341
|
-
projects: from.envelope.result.projects,
|
|
342
|
-
dependencies: from.envelope.result.dependencies,
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
projects: to.envelope.result.projects,
|
|
346
|
-
dependencies: to.envelope.result.dependencies,
|
|
347
|
-
},
|
|
348
|
-
);
|
|
349
|
-
const architectureChanged =
|
|
350
|
-
diff.addedProjects.length > 0 ||
|
|
351
|
-
diff.removedProjects.length > 0 ||
|
|
352
|
-
diff.changedProjects.length > 0 ||
|
|
353
|
-
diff.addedEdges.length > 0 ||
|
|
354
|
-
diff.removedEdges.length > 0;
|
|
355
|
-
|
|
356
|
-
// Code drift is a disclosure, so it is only asserted when every signal
|
|
357
|
-
// that could refute it is verifiable and unchanged: the architecture did
|
|
358
|
-
// not move, the policy was actually compared and did not change, and
|
|
359
|
-
// provenance advanced. A `null` policyChanged (one-sided, or neither
|
|
360
|
-
// snapshot carries a fingerprint) is "could not be compared", not "the
|
|
361
|
-
// same" — asserting code drift on an unverifiable policy would report a
|
|
362
|
-
// clean transition where the tool cannot look.
|
|
363
|
-
const codeDrift =
|
|
364
|
-
!architectureChanged && meta.policyChanged === false && meta.provenanceChanged === true;
|
|
365
|
-
|
|
366
|
-
transitions.push({
|
|
367
|
-
from: from.name,
|
|
368
|
-
to: to.name,
|
|
369
|
-
architectureChanged,
|
|
370
|
-
// A provider change is rendered with an empty diff (no graph change on
|
|
371
|
-
// top of a carrier change), a policy-only transition with null — so a
|
|
372
|
-
// consumer can tell "the carrier changed" from "only the record's
|
|
373
|
-
// interpretation changed".
|
|
374
|
-
changes: architectureChanged || meta.providerChanged ? diff : null,
|
|
375
|
-
policyChanged: meta.policyChanged,
|
|
376
|
-
providerChanged: meta.providerChanged,
|
|
377
|
-
codeDrift,
|
|
378
|
-
notes,
|
|
379
|
-
});
|
|
406
|
+
transitions.push(classifyTransition(files[i], files[i + 1]).record);
|
|
380
407
|
}
|
|
381
408
|
|
|
382
409
|
return { snapshots, transitions };
|
package/src/commands/policy.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* below argues the order, each arm, and what `profile`/`source` name.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
11
12
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
12
13
|
|
|
13
14
|
import { containmentViolation } from "../containment.mjs";
|
|
@@ -118,6 +119,62 @@ export async function resolvePolicy(options, commandContext, cwd) {
|
|
|
118
119
|
return resolved;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The policy load for a command that DESCRIBES the workspace rather than
|
|
124
|
+
* judging it — `graph`'s own arm, held here beside the ladder it wraps so
|
|
125
|
+
* every future descriptive surface (`./graph.mjs`'s MCP caller among them)
|
|
126
|
+
* answers from one copy of the "absent default law" decision rather than
|
|
127
|
+
* re-deriving it.
|
|
128
|
+
*
|
|
129
|
+
* What is skipped is the load of a file that is NOT THERE. A boundary config
|
|
130
|
+
* that exists and will not load still fails the run, because an absent law
|
|
131
|
+
* and a broken one must not report alike; a `--config`, a profile, and an
|
|
132
|
+
* inline `archkeep.json` policy are explicit declarations and stay loud.
|
|
133
|
+
* Every command that JUDGES against the law keeps loading it unconditionally
|
|
134
|
+
* through `resolvePolicy` — making it optional for those would turn a missing
|
|
135
|
+
* file into a silent no-law run.
|
|
136
|
+
*
|
|
137
|
+
* `boundaryConfigDeclared` is what keeps this guard to the un-overridden
|
|
138
|
+
* default, and it is load-bearing rather than belt-and-braces. The name
|
|
139
|
+
* alone cannot answer it: `commandContext.options.boundaryConfig` is a
|
|
140
|
+
* string BOTH when it came from `../options.mjs`'s `DEFAULT_OPTIONS` and
|
|
141
|
+
* when the consumer WROTE it into `nx.json`'s plugin options or
|
|
142
|
+
* `archkeep.json`, and a workspace is free to declare the convention
|
|
143
|
+
* filename itself, so comparing against the default would still read a
|
|
144
|
+
* deliberate declaration as an assumption. Measured on a committed native
|
|
145
|
+
* tree whose `archkeep.json` declares a `boundaryConfig` that file does not
|
|
146
|
+
* contain: skipping on name alone made `graph` exit 0 with no `policy` field
|
|
147
|
+
* — byte-identical to a workspace that never had a law — where the same tree
|
|
148
|
+
* with that file present but unparseable exited 3. A law someone named and
|
|
149
|
+
* then renamed or deleted is exactly the case that must stay loud, so the
|
|
150
|
+
* provenance survives the options layer instead
|
|
151
|
+
* (`../options.mjs`'s `resolveOptions`,
|
|
152
|
+
* `../providers/native/model.mjs`'s `normalizeNativeModel`, and
|
|
153
|
+
* `./context.mjs`'s three branches carry it; Moon answers `false` because it
|
|
154
|
+
* has no table to declare one in).
|
|
155
|
+
*
|
|
156
|
+
* @param {{config: string|null}} options The command's own parsed flags.
|
|
157
|
+
* @param {object} commandContext From `resolveCommandContext`.
|
|
158
|
+
* @param {string} cwd The process's working directory a relative `--config`
|
|
159
|
+
* resolves against.
|
|
160
|
+
* @returns {Promise<{config: object|null, profile: string|null, source: string|null}>}
|
|
161
|
+
* `null` config only on the absent-un-overridden-default arm.
|
|
162
|
+
* @throws {Error} through `resolvePolicy` for every law that is named but
|
|
163
|
+
* cannot be loaded.
|
|
164
|
+
*/
|
|
165
|
+
export async function resolveDescribedPolicy(options, commandContext, cwd) {
|
|
166
|
+
const workspaceDefault =
|
|
167
|
+
!options.config &&
|
|
168
|
+
!hasProfiles(commandContext.options) &&
|
|
169
|
+
commandContext.options.boundaryConfigDeclared === false &&
|
|
170
|
+
typeof commandContext.options.boundaryConfig === "string"
|
|
171
|
+
? resolve(commandContext.root, commandContext.options.boundaryConfig)
|
|
172
|
+
: null;
|
|
173
|
+
return workspaceDefault !== null && !existsSync(workspaceDefault)
|
|
174
|
+
? { config: null, profile: null, source: null }
|
|
175
|
+
: resolvePolicy(options, commandContext, cwd);
|
|
176
|
+
}
|
|
177
|
+
|
|
121
178
|
/**
|
|
122
179
|
* The four arms of the ladder, unguarded — `resolvePolicy` above wraps this
|
|
123
180
|
* with the one post-resolution refusal that needs both the provider and the
|
|
@@ -24,9 +24,7 @@
|
|
|
24
24
|
* timestamp is not.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
import { environmentForTree } from "../process.mjs";
|
|
27
|
+
import { runProcess } from "../process.mjs";
|
|
30
28
|
|
|
31
29
|
/**
|
|
32
30
|
* Resolves repository provenance from the workspace root.
|
|
@@ -49,22 +47,12 @@ import { environmentForTree } from "../process.mjs";
|
|
|
49
47
|
* @throws {Error} when `root` is a git repository with no commits.
|
|
50
48
|
*/
|
|
51
49
|
export function resolveProvenance(root) {
|
|
52
|
-
// G-09: every git spawn routes through the shared environment guard, so an
|
|
53
|
-
// ambient GIT_DIR/GIT_WORK_TREE from a wrapping tool (the editor hooks, an
|
|
54
|
-
// outer `git` call) can never make these spawns read a repository other
|
|
55
|
-
// than the tree at `root`.
|
|
56
|
-
const env = environmentForTree();
|
|
57
50
|
// First, the "is this even a git repository at all" question, asked before
|
|
58
51
|
// `rev-parse HEAD` so an unborn HEAD (a commitless repo) is distinguishable
|
|
59
52
|
// from "not a repo" — the two must not share the `null` answer, because
|
|
60
53
|
// only the first is a legitimate "no origin claim".
|
|
61
54
|
try {
|
|
62
|
-
|
|
63
|
-
cwd: root,
|
|
64
|
-
env,
|
|
65
|
-
encoding: "utf-8",
|
|
66
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
67
|
-
});
|
|
55
|
+
runProcess("git", ["rev-parse", "--is-inside-work-tree"], root);
|
|
68
56
|
} catch {
|
|
69
57
|
// git not available, or not a git repository. Return null — the envelope
|
|
70
58
|
// carries no origin claim rather than a false one.
|
|
@@ -72,12 +60,7 @@ export function resolveProvenance(root) {
|
|
|
72
60
|
}
|
|
73
61
|
let commit;
|
|
74
62
|
try {
|
|
75
|
-
commit =
|
|
76
|
-
cwd: root,
|
|
77
|
-
env,
|
|
78
|
-
encoding: "utf-8",
|
|
79
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
-
}).trim();
|
|
63
|
+
commit = runProcess("git", ["rev-parse", "--verify", "HEAD"], root).trim();
|
|
81
64
|
} catch {
|
|
82
65
|
// `--is-inside-work-tree` passed above, so a repo EXISTS here, and
|
|
83
66
|
// `--verify` (the two-argument form) failed to resolve HEAD. Two states
|
|
@@ -95,12 +78,7 @@ export function resolveProvenance(root) {
|
|
|
95
78
|
// means the HEAD ref is broken.
|
|
96
79
|
let reachable;
|
|
97
80
|
try {
|
|
98
|
-
reachable =
|
|
99
|
-
cwd: root,
|
|
100
|
-
env,
|
|
101
|
-
encoding: "utf-8",
|
|
102
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
103
|
-
}).trim();
|
|
81
|
+
reachable = runProcess("git", ["rev-list", "--count", "--all"], root).trim();
|
|
104
82
|
} catch {
|
|
105
83
|
// `rev-list --all` failing too is a degenerate repo; treat it as unborn
|
|
106
84
|
// rather than inventing a third class.
|
|
@@ -124,21 +102,11 @@ export function resolveProvenance(root) {
|
|
|
124
102
|
// the tree.
|
|
125
103
|
let remote = null;
|
|
126
104
|
try {
|
|
127
|
-
const remotes =
|
|
128
|
-
cwd: root,
|
|
129
|
-
env,
|
|
130
|
-
encoding: "utf-8",
|
|
131
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
132
|
-
}).trim();
|
|
105
|
+
const remotes = runProcess("git", ["remote"], root).trim();
|
|
133
106
|
if (remotes) {
|
|
134
107
|
// Use the first remote's URL — typically "origin".
|
|
135
108
|
const firstRemote = remotes.split("\n")[0].trim();
|
|
136
|
-
remote =
|
|
137
|
-
cwd: root,
|
|
138
|
-
env,
|
|
139
|
-
encoding: "utf-8",
|
|
140
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
141
|
-
}).trim();
|
|
109
|
+
remote = runProcess("git", ["remote", "get-url", firstRemote], root).trim();
|
|
142
110
|
}
|
|
143
111
|
} catch {
|
|
144
112
|
// No remotes configured — `remote` stays null.
|
|
@@ -147,12 +115,7 @@ export function resolveProvenance(root) {
|
|
|
147
115
|
// Dirty: any uncommitted change to tracked files means the working tree
|
|
148
116
|
// does not match the commit. A baseline from a dirty tree is not a
|
|
149
117
|
// reproducible claim about that commit.
|
|
150
|
-
const status =
|
|
151
|
-
cwd: root,
|
|
152
|
-
env,
|
|
153
|
-
encoding: "utf-8",
|
|
154
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
155
|
-
}).trim();
|
|
118
|
+
const status = runProcess("git", ["status", "--porcelain"], root).trim();
|
|
156
119
|
const dirty = status.length > 0;
|
|
157
120
|
|
|
158
121
|
return { commit, remote, dirty };
|