@intentius/chant 0.100.0 → 0.101.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/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/build-options.d.ts +2 -0
- package/dist/cli/build-options.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +8 -0
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon.d.ts +24 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-digest.d.ts +26 -5
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lint/config.d.ts +4 -4
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +2 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/serializer.d.ts +8 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/telemetry-attribution.d.ts +77 -0
- package/dist/telemetry-attribution.d.ts.map +1 -0
- package/dist/workspace/agent-cli.d.ts +83 -0
- package/dist/workspace/agent-cli.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes.d.ts +8 -1
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/checks/links.d.ts +1 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/live.d.ts +40 -0
- package/dist/workspace/checks/live.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +21 -2
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +63 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +1 -1
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +32 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +138 -3
- package/dist/workspace/export-cli.d.ts +12 -0
- package/dist/workspace/export-cli.d.ts.map +1 -0
- package/dist/workspace/export.d.ts +145 -0
- package/dist/workspace/export.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/import.d.ts +73 -0
- package/dist/workspace/import.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +6 -2
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
- package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-adopt.d.ts +106 -0
- package/dist/workspace/lineage-adopt.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +9 -2
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-cli.d.ts +6 -1
- package/dist/workspace/lineage-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-hash-index.d.ts +110 -0
- package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +10 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +147 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.d.ts +15 -3
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-provenance.d.ts +18 -0
- package/dist/workspace/lineage-provenance.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +30 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-versions.d.ts +116 -0
- package/dist/workspace/lineage-versions.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +31 -5
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/ls-generated.d.ts +37 -0
- package/dist/workspace/ls-generated.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/nested-graph.d.ts +56 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -0
- package/dist/workspace/nesting.d.ts +21 -0
- package/dist/workspace/nesting.d.ts.map +1 -0
- package/dist/workspace/pin-cli.d.ts +10 -0
- package/dist/workspace/pin-cli.d.ts.map +1 -0
- package/dist/workspace/pin-integrity.d.ts +51 -0
- package/dist/workspace/pin-integrity.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +26 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +7 -11
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +30 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +5 -2
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +22 -4
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +43 -5
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/returns.d.ts +129 -0
- package/dist/workspace/returns.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/template-manifest.d.ts +11 -3
- package/dist/workspace/template-manifest.d.ts.map +1 -1
- package/dist/workspace/trust/attestor.d.ts +8 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -1
- package/dist/workspace/trust/dsse.d.ts +58 -0
- package/dist/workspace/trust/dsse.d.ts.map +1 -0
- package/dist/workspace/trust/evidence-cli.d.ts +66 -0
- package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
- package/dist/workspace/trust/evidence.d.ts +93 -0
- package/dist/workspace/trust/evidence.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +54 -1
- package/dist/workspace/trust/policy.d.ts.map +1 -1
- package/dist/workspace/trust/provenance.d.ts +21 -1
- package/dist/workspace/trust/provenance.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.d.ts +132 -0
- package/dist/workspace/trust/rotation.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/signers-cli.d.ts +49 -0
- package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +12 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.d.ts +13 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -1
- package/dist/workspace/trust/verify.d.ts +15 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/write-scope.d.ts +199 -0
- package/dist/workspace/write-scope.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/build.ts +8 -0
- package/src/cli/build-options.ts +4 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/import-live.test.ts +69 -1
- package/src/cli/commands/import.ts +48 -22
- package/src/cli/handlers/fan-out.test.ts +6 -6
- package/src/cli/handlers/fan-out.ts +2 -1
- package/src/cli/handlers/graph.test.ts +42 -0
- package/src/cli/handlers/graph.ts +22 -0
- package/src/cli/handlers/operator.ts +1 -1
- package/src/cli/main.test.ts +32 -0
- package/src/cli/main.ts +103 -6
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +33 -2
- package/src/cli/registry.ts +22 -0
- package/src/cli/serve-mcp-workspace.test.ts +1 -1
- package/src/codegen/release-wiring.test.ts +5 -1
- package/src/components/fan-out-output.test.ts +1 -1
- package/src/components/fan-out.test.ts +1 -1
- package/src/components/promote.test.ts +1 -1
- package/src/config.ts +12 -0
- package/src/content-digest.test.ts +2 -2
- package/src/lexicon.ts +25 -1
- package/src/lifecycle/gate-ledger.test.ts +14 -0
- package/src/lifecycle/gate-ledger.ts +2 -1
- package/src/lifecycle/plan-digest.test.ts +54 -3
- package/src/lifecycle/plan-digest.ts +38 -8
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/propose-upgrade.ts +8 -5
- package/src/op/gate-approval.test.ts +17 -0
- package/src/op/gate.ts +3 -3
- package/src/op/index.ts +1 -1
- package/src/serializer.ts +9 -0
- package/src/telemetry-attribution.test.ts +91 -0
- package/src/telemetry-attribution.ts +145 -0
- package/src/workspace/agent-cli.ts +134 -0
- package/src/workspace/agent.schema.json +356 -0
- package/src/workspace/behold-kinds.test.ts +1 -1
- package/src/workspace/changes-cli.ts +5 -0
- package/src/workspace/changes.schema.json +179 -1
- package/src/workspace/changes.ts +58 -4
- package/src/workspace/check-live.test.ts +192 -0
- package/src/workspace/check.schema.json +64 -0
- package/src/workspace/checks/links.ts +23 -2
- package/src/workspace/checks/live.ts +113 -0
- package/src/workspace/checks.test.ts +2 -2
- package/src/workspace/checks.ts +20 -3
- package/src/workspace/compose-graph.test.ts +47 -0
- package/src/workspace/compose-graph.ts +120 -3
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
- package/src/workspace/declaration.schema.json +138 -3
- package/src/workspace/declaration.ts +106 -1
- package/src/workspace/declared-kinds.test.ts +33 -0
- package/src/workspace/evidence.schema.json +279 -0
- package/src/workspace/export-cli.ts +180 -0
- package/src/workspace/export-import.test.ts +282 -0
- package/src/workspace/export.ts +486 -0
- package/src/workspace/graph-cli.ts +47 -1
- package/src/workspace/graph-contract.test.ts +89 -4
- package/src/workspace/graph.schema.json +206 -1
- package/src/workspace/import.ts +325 -0
- package/src/workspace/kinds.test.ts +5 -5
- package/src/workspace/kinds.ts +25 -3
- package/src/workspace/lineage-adopt-cli.ts +103 -0
- package/src/workspace/lineage-adopt.test.ts +552 -0
- package/src/workspace/lineage-adopt.ts +452 -0
- package/src/workspace/lineage-check.ts +26 -5
- package/src/workspace/lineage-cli.ts +12 -2
- package/src/workspace/lineage-hash-index.ts +305 -0
- package/src/workspace/lineage-init.test.ts +9 -0
- package/src/workspace/lineage-init.ts +29 -9
- package/src/workspace/lineage-lock.ts +54 -0
- package/src/workspace/lineage-migrations.test.ts +27 -0
- package/src/workspace/lineage-migrations.ts +44 -14
- package/src/workspace/lineage-provenance.ts +40 -0
- package/src/workspace/lineage-upgrade-cli.ts +9 -5
- package/src/workspace/lineage-upgrade.test.ts +92 -1
- package/src/workspace/lineage-upgrade.ts +111 -16
- package/src/workspace/lineage-versions.ts +348 -0
- package/src/workspace/links.test.ts +121 -2
- package/src/workspace/links.ts +97 -6
- package/src/workspace/ls-contract.test.ts +84 -1
- package/src/workspace/ls-generated.ts +111 -0
- package/src/workspace/ls.schema.json +19 -0
- package/src/workspace/ls.ts +8 -1
- package/src/workspace/member-commands.ts +3 -1
- package/src/workspace/nested-graph.test.ts +176 -0
- package/src/workspace/nested-graph.ts +169 -0
- package/src/workspace/nesting.ts +37 -0
- package/src/workspace/pin-cli.test.ts +71 -0
- package/src/workspace/pin-cli.ts +57 -0
- package/src/workspace/pin-integrity.test.ts +121 -0
- package/src/workspace/pin-integrity.ts +104 -0
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/read-contract.test.ts +24 -0
- package/src/workspace/reason-codes.test.ts +10 -0
- package/src/workspace/reason-codes.ts +29 -2
- package/src/workspace/record-sessions.ts +12 -14
- package/src/workspace/records-amend.schema.json +4 -0
- package/src/workspace/records-cli.ts +93 -12
- package/src/workspace/records-close.schema.json +6 -2
- package/src/workspace/records-close.ts +10 -2
- package/src/workspace/records-formats.test.ts +13 -5
- package/src/workspace/records-new.schema.json +4 -0
- package/src/workspace/records-review.schema.json +4 -0
- package/src/workspace/records-sessions.test.ts +23 -6
- package/src/workspace/records-write.test.ts +65 -8
- package/src/workspace/records-write.ts +70 -6
- package/src/workspace/records.schema.json +55 -3
- package/src/workspace/records.ts +96 -5
- package/src/workspace/returns.ts +328 -0
- package/src/workspace/signers.schema.json +206 -0
- package/src/workspace/status-gates.ts +2 -1
- package/src/workspace/template-manifest.ts +22 -4
- package/src/workspace/trust/attestor.ts +15 -0
- package/src/workspace/trust/dsse.ts +134 -0
- package/src/workspace/trust/evidence-cli.ts +195 -0
- package/src/workspace/trust/evidence.test.ts +241 -0
- package/src/workspace/trust/evidence.ts +207 -0
- package/src/workspace/trust/policy.ts +110 -3
- package/src/workspace/trust/provenance.ts +41 -4
- package/src/workspace/trust/record-seal.test.ts +1 -1
- package/src/workspace/trust/rotation.test.ts +258 -0
- package/src/workspace/trust/rotation.ts +336 -0
- package/src/workspace/trust/seal.ts +11 -0
- package/src/workspace/trust/signers-cli.ts +178 -0
- package/src/workspace/trust/ssh-commit.ts +53 -4
- package/src/workspace/trust/test-repo.ts +18 -0
- package/src/workspace/trust/trust.test.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +1 -0
- package/src/workspace/trust/verify.ts +22 -0
- package/src/workspace/work-evidence.schema.json +4 -0
- package/src/workspace/write-scope.test.ts +340 -0
- package/src/workspace/write-scope.ts +448 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace adopt-lineage`: give a scope a git lineage it can be
|
|
3
|
+
* upgraded from (#2551, D5, D9, requirement P7).
|
|
4
|
+
*
|
|
5
|
+
* Two cases, told apart by what the lock already holds for the scope.
|
|
6
|
+
*
|
|
7
|
+
* A scope with no lineage, such as a project made before the lock existed or
|
|
8
|
+
* copied by hand from a template or a fork of one, has no record of where its
|
|
9
|
+
* files came from. Adoption recovers it from the template's own versions:
|
|
10
|
+
*
|
|
11
|
+
* 1. Compute the template's hash index from its tags (./lineage-hash-index.ts),
|
|
12
|
+
* or reuse a cached copy for the tags whose commit it still names.
|
|
13
|
+
* 2. Score each version against the scope: how many of the template's files
|
|
14
|
+
* the scope holds byte for byte. The best version wins; ties go to the
|
|
15
|
+
* version whose files the scope accounts for best, then to the oldest.
|
|
16
|
+
* 3. Re-check a winner that came from the cache against the template itself,
|
|
17
|
+
* so a stale or forged cache never reaches the lock.
|
|
18
|
+
* 4. Record a lineage pinned to that version. Each file the template has
|
|
19
|
+
* there is recorded with the template's hash, after the template's
|
|
20
|
+
* parameters are substituted (#2627), as its merge base, so a file the
|
|
21
|
+
* scope edited shows as edited and merges on upgrade like any other.
|
|
22
|
+
* 5. Record the adoption under P7: the exact commit range it vouches for,
|
|
23
|
+
* everything up to HEAD, goes into the lineage and into
|
|
24
|
+
* `.chant/trust.json`'s `adopted` list. That file is policy, read at the
|
|
25
|
+
* base revision, and editing it is a protected write, so the range counts
|
|
26
|
+
* only once an admin has merged it. The scope then reads as `adopted`
|
|
27
|
+
* (./lineage-provenance.ts).
|
|
28
|
+
*
|
|
29
|
+
* A scope with a directory lineage (#2647) already has a lineage, but no
|
|
30
|
+
* history to rebuild a merge base from once the directory changes. The
|
|
31
|
+
* studio's boxes are made this way, with `chant init --from ~/box/template`,
|
|
32
|
+
* and a kit release replaces that directory. Adoption moves such a scope onto
|
|
33
|
+
* the git repository the directory was copied from: every candidate version
|
|
34
|
+
* is rendered with the parameters the lock recorded, and only a version that
|
|
35
|
+
* reproduces every hash the lock recorded qualifies. The merge base is then
|
|
36
|
+
* the same files, so the lineage keeps its files, parameters and migrations,
|
|
37
|
+
* and only its source, ref and address change. Nothing about the scope's
|
|
38
|
+
* history is claimed, so no commit range is admitted.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { execFileSync } from "node:child_process";
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
43
|
+
import { dirname, join, resolve } from "node:path";
|
|
44
|
+
import { computeHashIndex, indexExcludes, TemplateTags, type HashIndex, type HashIndexEntry } from "./lineage-hash-index";
|
|
45
|
+
import { parseTemplateSource, portableUrl, readTemplateTree } from "./lineage-init";
|
|
46
|
+
import {
|
|
47
|
+
LOCK_FILE,
|
|
48
|
+
LockError,
|
|
49
|
+
contentDigest,
|
|
50
|
+
declaredFilesAt,
|
|
51
|
+
emptyLock,
|
|
52
|
+
fileEntries,
|
|
53
|
+
fileHash,
|
|
54
|
+
readLock,
|
|
55
|
+
scopeKey,
|
|
56
|
+
writeLock,
|
|
57
|
+
type Adoption,
|
|
58
|
+
type Lineage,
|
|
59
|
+
} from "./lineage-lock";
|
|
60
|
+
import { splitMigrations } from "./lineage-migrations";
|
|
61
|
+
import { carryParameters, readManifest, resolveParameters, substituteParameters } from "./template-manifest";
|
|
62
|
+
import { repinSubstituted, type RepinnedRecord } from "./template-pins";
|
|
63
|
+
|
|
64
|
+
/** Where the trust policy lives, relative to the repository root (./trust/policy.ts). */
|
|
65
|
+
const TRUST_FILE = ".chant/trust.json";
|
|
66
|
+
|
|
67
|
+
export interface AdoptOptions {
|
|
68
|
+
/** The directory that holds (or will hold) `.chant/workspace.lock.json`. */
|
|
69
|
+
root: string;
|
|
70
|
+
/** The scope to adopt, relative to `root`. Defaults to `"."`. */
|
|
71
|
+
scope?: string;
|
|
72
|
+
/** `<repo>[@<ref>][#<member>]`: the template. With a ref, adopt at exactly that version. */
|
|
73
|
+
from: string;
|
|
74
|
+
/** Only tags matching this glob are candidates, such as `kit-v*`. */
|
|
75
|
+
tags?: string;
|
|
76
|
+
/** A cached hash index, such as a copy the template's CI published. */
|
|
77
|
+
cache?: HashIndex;
|
|
78
|
+
/** `--param name=value` values for a template with a `chant.template.json`; only for a scope with no lineage. */
|
|
79
|
+
params?: Record<string, string>;
|
|
80
|
+
dryRun?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VersionScore {
|
|
84
|
+
tag: string;
|
|
85
|
+
commit: string;
|
|
86
|
+
/** Files the template has at the version. */
|
|
87
|
+
files: number;
|
|
88
|
+
identical: number;
|
|
89
|
+
edited: number;
|
|
90
|
+
missing: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AdoptionResult {
|
|
94
|
+
scope: string;
|
|
95
|
+
/** `files` for a scope with no lineage, `lineage` for a directory lineage moved onto git. */
|
|
96
|
+
by: Adoption["by"];
|
|
97
|
+
template: string;
|
|
98
|
+
chosen: VersionScore;
|
|
99
|
+
/** The next best versions that do not tie with the chosen one, best first, at most three. */
|
|
100
|
+
alternatives: VersionScore[];
|
|
101
|
+
/** Other versions that score exactly as the chosen one does. `@<ref>` in `--from` picks one of them. */
|
|
102
|
+
ties: string[];
|
|
103
|
+
/** How many versions were compared. */
|
|
104
|
+
compared: number;
|
|
105
|
+
/** The files, by how the scope holds them (for `lineage`, by how the lock recorded them). */
|
|
106
|
+
identical: string[];
|
|
107
|
+
edited: string[];
|
|
108
|
+
missing: string[];
|
|
109
|
+
index: Adoption["index"];
|
|
110
|
+
/** The lineage the lock records. */
|
|
111
|
+
lineage: Lineage;
|
|
112
|
+
/** The `.chant/trust.json` entry the adoption adds, for `files`. Null when it is already there or for `lineage`. */
|
|
113
|
+
trust: { path: string; entry: { to: string; note: string } } | null;
|
|
114
|
+
/** Whether the lock (and the trust entry) were written: false for a dry run. */
|
|
115
|
+
written: boolean;
|
|
116
|
+
/** Whether a `.gitignore` covers the lock, so it would not be committed without `git add -f`. */
|
|
117
|
+
lockIgnored: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function gitRaw(cwd: string, args: string[]): string {
|
|
121
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isIgnored(root: string, path: string): boolean {
|
|
125
|
+
try {
|
|
126
|
+
gitRaw(root, ["check-ignore", "-q", "--no-index", "--", path]);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Score one version against a lookup of the scope's hashes. */
|
|
134
|
+
function score(entry: { tag: string; commit: string; files: Record<string, string> }, have: (path: string) => string | undefined): VersionScore {
|
|
135
|
+
let identical = 0;
|
|
136
|
+
let edited = 0;
|
|
137
|
+
let missing = 0;
|
|
138
|
+
for (const [path, sha] of Object.entries(entry.files)) {
|
|
139
|
+
const h = have(path);
|
|
140
|
+
if (h === undefined) missing++;
|
|
141
|
+
else if (h === sha) identical++;
|
|
142
|
+
else edited++;
|
|
143
|
+
}
|
|
144
|
+
return { tag: entry.tag, commit: entry.commit, files: Object.keys(entry.files).length, identical, edited, missing };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type Ranked = VersionScore & { order: number };
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Best first: most identical files, then fewest template files the scope does
|
|
151
|
+
* not hold as they are, then the oldest version. Versions that tie hold the
|
|
152
|
+
* same files, so the scope fits any of them. Adopting the oldest means an
|
|
153
|
+
* upgrade replays every migration since; one that no longer applies refuses
|
|
154
|
+
* the upgrade, where adopting the newest would skip it without a word.
|
|
155
|
+
*/
|
|
156
|
+
function rank(a: Ranked, b: Ranked): number {
|
|
157
|
+
return b.identical - a.identical || a.edited + a.missing - (b.edited + b.missing) || a.order - b.order;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ties = (best: VersionScore) => (r: VersionScore) => r.identical === best.identical && r.edited + r.missing === best.edited + best.missing;
|
|
161
|
+
const plain = ({ order: _o, ...s }: Ranked): VersionScore => s;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* HEAD of the repository holding the scope, once the scope and the lock have
|
|
165
|
+
* no uncommitted changes: an adoption names an exact commit, and uncommitted
|
|
166
|
+
* files are in none.
|
|
167
|
+
*/
|
|
168
|
+
function adoptedAt(root: string, scope: string): { repo: string; head: string } {
|
|
169
|
+
let repo: string;
|
|
170
|
+
let head: string;
|
|
171
|
+
try {
|
|
172
|
+
repo = gitRaw(root, ["rev-parse", "--show-toplevel"]).trim();
|
|
173
|
+
head = gitRaw(root, ["rev-parse", "HEAD"]).trim();
|
|
174
|
+
} catch {
|
|
175
|
+
throw new LockError("adopt-lineage records the exact commit it adopts at, so the scope must be in a git repository with at least one commit");
|
|
176
|
+
}
|
|
177
|
+
// Untrimmed: each porcelain line starts with its two status columns, which may be spaces.
|
|
178
|
+
const dirty = gitRaw(root, ["status", "--porcelain", "--untracked-files=all", "--", scope === "." ? "." : scope, LOCK_FILE])
|
|
179
|
+
.split("\n")
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
if (dirty.length > 0) {
|
|
182
|
+
throw new LockError(
|
|
183
|
+
`the scope has uncommitted changes (${dirty.slice(0, 5).map((l) => l.slice(3)).join(", ")}${dirty.length > 5 ? ", …" : ""}). Commit them first: an adoption names an exact commit, and they are in none.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return { repo, head };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A version's files as a project made from it holds them: parameters substituted, manifest and migrations left out. */
|
|
190
|
+
function instantiate(
|
|
191
|
+
raw: Map<string, Buffer>,
|
|
192
|
+
values: (manifest: ReturnType<typeof readManifest>) => Record<string, string>,
|
|
193
|
+
): { files: Map<string, Buffer>; parameters: Record<string, string>; repinned: RepinnedRecord[] } {
|
|
194
|
+
const manifest = readManifest(raw);
|
|
195
|
+
const parameters = values(manifest);
|
|
196
|
+
const { files, repinned } = repinSubstituted(raw, substituteParameters(raw, manifest, parameters), manifest?.files ?? []);
|
|
197
|
+
const kept = new Map<string, Buffer>();
|
|
198
|
+
for (const [path, data] of splitMigrations(files).files) if (!indexExcludes(path)) kept.set(path, data);
|
|
199
|
+
return { files: kept, parameters, repinned };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readRaw(tags: TemplateTags, entry: HashIndexEntry, member: string | undefined, label: string): Map<string, Buffer> {
|
|
203
|
+
const read = readTemplateTree(tags.scratch, entry.commit, member, `${label}@${entry.tag}`);
|
|
204
|
+
return new Map([...read.files].map(([path, f]) => [path, f.data]));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Add the adoption's range to `.chant/trust.json`, keeping everything else in it. Returns null when an entry already covers it. */
|
|
208
|
+
function trustEntry(repo: string, head: string, note: string): { path: string; entry: { to: string; note: string }; text: string } | null {
|
|
209
|
+
const abs = join(repo, TRUST_FILE);
|
|
210
|
+
let config: Record<string, unknown> = { schema: 1 };
|
|
211
|
+
if (existsSync(abs)) {
|
|
212
|
+
try {
|
|
213
|
+
config = JSON.parse(readFileSync(abs, "utf-8")) as Record<string, unknown>;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw new LockError(`${TRUST_FILE} is not valid JSON, so the adoption cannot be added to it: ${err instanceof Error ? err.message : String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const adopted = Array.isArray(config.adopted) ? (config.adopted as Array<{ to?: unknown }>) : [];
|
|
219
|
+
if (adopted.some((r) => r.to === head)) return null;
|
|
220
|
+
const entry = { to: head, note };
|
|
221
|
+
config.adopted = [...adopted, entry];
|
|
222
|
+
return { path: TRUST_FILE, entry, text: JSON.stringify(config, null, 2) + "\n" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Adopt a lineage for the scope, and record it unless `dryRun`. Throws a
|
|
227
|
+
* {@link LockError} when the scope has a lineage that is not a directory
|
|
228
|
+
* lineage, when no version fits, or when the template cannot be read.
|
|
229
|
+
*/
|
|
230
|
+
export function adoptLineage(options: AdoptOptions): AdoptionResult {
|
|
231
|
+
const root = resolve(options.root);
|
|
232
|
+
const scope = scopeKey(options.scope ?? ".");
|
|
233
|
+
const scopeDir = join(root, scope);
|
|
234
|
+
if (!existsSync(scopeDir) || !statSync(scopeDir).isDirectory()) throw new LockError(`scope "${scope}" is not a directory under ${root}`);
|
|
235
|
+
const lock = readLock(root) ?? emptyLock();
|
|
236
|
+
const existing = lock.scopes[scope];
|
|
237
|
+
if (existing && existing.source.type !== "dir") {
|
|
238
|
+
throw new LockError(
|
|
239
|
+
`${LOCK_FILE} already has a lineage for scope "${scope}" (${existing.template}, ${existing.source.type}). adopt-lineage gives a lineage to a scope with none, or moves a directory lineage onto git`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (existing && existing.manualSteps.length > 0) {
|
|
243
|
+
throw new LockError(`scope "${scope}" has ${existing.manualSteps.length} open manual step(s); resolve them before moving its lineage`);
|
|
244
|
+
}
|
|
245
|
+
if (existing && options.params && Object.keys(options.params).length > 0) {
|
|
246
|
+
throw new LockError(`scope "${scope}" already records its parameters; --param is only for a scope with no lineage`);
|
|
247
|
+
}
|
|
248
|
+
const spec = parseTemplateSource(options.from, root, "--from");
|
|
249
|
+
const { repo, head } = adoptedAt(root, scope);
|
|
250
|
+
const label = spec.member ? `${spec.repo}#${spec.member}` : spec.repo;
|
|
251
|
+
const tags = new TemplateTags(spec.url, spec.member, label);
|
|
252
|
+
try {
|
|
253
|
+
const result = existing
|
|
254
|
+
? fromDirectoryLineage(tags, existing, { spec, label, pattern: options.tags, head })
|
|
255
|
+
: fromFiles(tags, scopeDir, { spec, label, pattern: options.tags, cache: options.cache, params: options.params ?? {}, head });
|
|
256
|
+
const lineage: Lineage = { ...result.lineage, source: { type: "git", repo: spec.repo, url: portableUrl(spec.url, root), ...(spec.member ? { path: spec.member } : {}) } };
|
|
257
|
+
const trust = lineage.adoption!.by === "files" ? trustEntry(repo, head, `adopt-lineage ${scope} ${spec.id}@${result.chosen.tag}`) : null;
|
|
258
|
+
if (!options.dryRun) {
|
|
259
|
+
lock.scopes[scope] = lineage;
|
|
260
|
+
writeLock(root, lock);
|
|
261
|
+
if (trust) {
|
|
262
|
+
mkdirSync(dirname(join(repo, trust.path)), { recursive: true });
|
|
263
|
+
writeFileSync(join(repo, trust.path), trust.text);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
...result,
|
|
268
|
+
scope,
|
|
269
|
+
template: spec.id,
|
|
270
|
+
lineage,
|
|
271
|
+
trust: trust ? { path: trust.path, entry: trust.entry } : null,
|
|
272
|
+
written: !options.dryRun,
|
|
273
|
+
lockIgnored: isIgnored(root, LOCK_FILE),
|
|
274
|
+
};
|
|
275
|
+
} finally {
|
|
276
|
+
tags.dispose();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type Found = Omit<AdoptionResult, "scope" | "template" | "trust" | "written" | "lockIgnored">;
|
|
281
|
+
|
|
282
|
+
/** A scope with no lineage: matched by its files. */
|
|
283
|
+
function fromFiles(
|
|
284
|
+
tags: TemplateTags,
|
|
285
|
+
scopeDir: string,
|
|
286
|
+
o: { spec: ReturnType<typeof parseTemplateSource>; label: string; pattern?: string; cache?: HashIndex; params: Record<string, string>; head: string },
|
|
287
|
+
): Found {
|
|
288
|
+
const hashes = new Map<string, string | undefined>();
|
|
289
|
+
const local = (path: string): string | undefined => {
|
|
290
|
+
if (!hashes.has(path)) {
|
|
291
|
+
const abs = join(scopeDir, path);
|
|
292
|
+
hashes.set(path, existsSync(abs) && statSync(abs).isFile() ? fileHash(readFileSync(abs)) : undefined);
|
|
293
|
+
}
|
|
294
|
+
return hashes.get(path);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
let { index, cached } = computeHashIndex(tags, o.spec.id, { pattern: o.pattern, cache: o.cache, only: o.spec.ref });
|
|
298
|
+
for (;;) {
|
|
299
|
+
const ranked = index.tags.map((entry, order) => ({ ...score(entry, local), order })).sort(rank);
|
|
300
|
+
const best = ranked[0];
|
|
301
|
+
if (!best || best.identical === 0) {
|
|
302
|
+
throw new LockError(
|
|
303
|
+
`no version of ${o.label} shares a file with the scope (compared ${index.tags.length}); check --from${o.spec.member ? "" : " and its #<member>"}, or --tags`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const entry = index.tags[best.order];
|
|
307
|
+
if (cached.has(entry.tag)) {
|
|
308
|
+
// Re-check a cached winner against the template itself: the lock records only hashes chant computed.
|
|
309
|
+
tags.fetchTags([entry.tag]);
|
|
310
|
+
const fresh = tags.entry({ tag: entry.tag, commit: entry.commit });
|
|
311
|
+
if (fresh === null || fresh.tree !== entry.tree || JSON.stringify(fresh.files) !== JSON.stringify(entry.files)) {
|
|
312
|
+
// A stale or wrong cache: drop it and compute every version from the template.
|
|
313
|
+
({ index, cached } = computeHashIndex(tags, o.spec.id, { pattern: o.pattern, only: o.spec.ref }));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const usedCache = cached.size > 0;
|
|
318
|
+
|
|
319
|
+
// The version as a project made from it holds it: the scope's own parameter values substituted (#2627).
|
|
320
|
+
const made = instantiate(readRaw(tags, entry, o.spec.member, o.label), (manifest) => resolveParameters(manifest, o.params));
|
|
321
|
+
const files = fileEntries(made.files, declaredFilesAt(scopeDir));
|
|
322
|
+
const identical: string[] = [];
|
|
323
|
+
const edited: string[] = [];
|
|
324
|
+
const missing: string[] = [];
|
|
325
|
+
for (const [path, e] of Object.entries(files)) {
|
|
326
|
+
const have = local(path);
|
|
327
|
+
(have === undefined ? missing : have === e.sha256 ? identical : edited).push(path);
|
|
328
|
+
}
|
|
329
|
+
const match = { files: Object.keys(files).length, identical: identical.length, edited: edited.length, missing: missing.length };
|
|
330
|
+
const lineage: Lineage = {
|
|
331
|
+
kind: "template",
|
|
332
|
+
template: o.spec.id,
|
|
333
|
+
source: { type: "git", repo: o.spec.repo, url: o.spec.url },
|
|
334
|
+
ref: entry.tag,
|
|
335
|
+
address: { digest: contentDigest(made.files), commit: entry.commit, tree: entry.tree },
|
|
336
|
+
parameters: made.parameters,
|
|
337
|
+
...(made.repinned.length > 0 ? { repinned: made.repinned } : {}),
|
|
338
|
+
migrations: [],
|
|
339
|
+
files,
|
|
340
|
+
manualSteps: [],
|
|
341
|
+
adoption: { by: "files", commits: { to: o.head }, match, index: usedCache ? "cache" : "computed" },
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
by: "files",
|
|
345
|
+
chosen: { tag: entry.tag, commit: entry.commit, ...match },
|
|
346
|
+
alternatives: ranked.slice(1).filter((r) => !ties(best)(r)).slice(0, 3).map(plain),
|
|
347
|
+
ties: ranked.slice(1).filter(ties(best)).map((r) => r.tag),
|
|
348
|
+
compared: index.tags.length,
|
|
349
|
+
identical,
|
|
350
|
+
edited,
|
|
351
|
+
missing,
|
|
352
|
+
index: lineage.adoption!.index,
|
|
353
|
+
lineage,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A directory lineage moved onto git: the version must reproduce, with the
|
|
360
|
+
* recorded parameters, the hash of every file the lock records (generated
|
|
361
|
+
* files aside, since they are rebuilt and never merged).
|
|
362
|
+
*/
|
|
363
|
+
function fromDirectoryLineage(
|
|
364
|
+
tags: TemplateTags,
|
|
365
|
+
existing: Lineage,
|
|
366
|
+
o: { spec: ReturnType<typeof parseTemplateSource>; label: string; pattern?: string; head: string },
|
|
367
|
+
): Found {
|
|
368
|
+
const { index } = computeHashIndex(tags, o.spec.id, { pattern: o.pattern, only: o.spec.ref });
|
|
369
|
+
const recorded = Object.entries(existing.files).filter(([, e]) => e.class !== "generated");
|
|
370
|
+
const candidates: Array<Ranked & { entry: HashIndexEntry; made: ReturnType<typeof instantiate>; exact: boolean; differ: string[] }> = [];
|
|
371
|
+
index.tags.forEach((entry, order) => {
|
|
372
|
+
let made: ReturnType<typeof instantiate>;
|
|
373
|
+
try {
|
|
374
|
+
made = instantiate(readRaw(tags, entry, o.spec.member, o.label), (manifest) => carryParameters(manifest, existing.parameters));
|
|
375
|
+
} catch {
|
|
376
|
+
// A version whose manifest the recorded parameters do not satisfy cannot be the one the scope was made from.
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const at = new Map([...made.files].map(([p, d]) => [p, fileHash(d)]));
|
|
380
|
+
const s = score({ tag: entry.tag, commit: entry.commit, files: Object.fromEntries(recorded.map(([p, e]) => [p, e.sha256])) }, (p) => at.get(p));
|
|
381
|
+
const differ = recorded.filter(([p, e]) => at.get(p) !== e.sha256).map(([p]) => p);
|
|
382
|
+
candidates.push({ ...s, files: made.files.size, order, entry, made, exact: contentDigest(made.files) === existing.address?.digest, differ });
|
|
383
|
+
});
|
|
384
|
+
const fits = candidates.filter((c) => c.differ.length === 0).sort((a, b) => Number(b.exact) - Number(a.exact) || a.order - b.order);
|
|
385
|
+
if (fits.length === 0) {
|
|
386
|
+
const near = [...candidates].sort((a, b) => a.differ.length - b.differ.length || a.order - b.order)[0];
|
|
387
|
+
throw new LockError(
|
|
388
|
+
`no version of ${o.label} reproduces the files the scope was made from (compared ${index.tags.length}${o.pattern ? `, tags ${o.pattern}` : ""})` +
|
|
389
|
+
(near ? `; the nearest, ${near.tag}, differs in ${near.differ.slice(0, 5).join(", ")}${near.differ.length > 5 ? ", …" : ""}` : "") +
|
|
390
|
+
`. Name the commit the directory was copied from with --from <repo>@<ref>, or upgrade from a directory that still holds those files.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const chosen = fits[0];
|
|
394
|
+
const files = Object.keys(existing.files);
|
|
395
|
+
const match = { files: chosen.files, identical: recorded.length, edited: 0, missing: 0 };
|
|
396
|
+
const lineage: Lineage = {
|
|
397
|
+
...existing,
|
|
398
|
+
template: o.spec.id,
|
|
399
|
+
source: { type: "git", repo: o.spec.repo, url: o.spec.url },
|
|
400
|
+
ref: chosen.entry.tag,
|
|
401
|
+
address: { digest: contentDigest(chosen.made.files), commit: chosen.entry.commit, tree: chosen.entry.tree },
|
|
402
|
+
adoption: {
|
|
403
|
+
by: "lineage",
|
|
404
|
+
commits: { to: o.head },
|
|
405
|
+
match,
|
|
406
|
+
index: "computed",
|
|
407
|
+
previous: { template: existing.template, source: existing.source },
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
const same = (c: (typeof fits)[number]) => c.exact === chosen.exact;
|
|
411
|
+
return {
|
|
412
|
+
by: "lineage",
|
|
413
|
+
chosen: { tag: chosen.tag, commit: chosen.commit, ...match },
|
|
414
|
+
alternatives: [],
|
|
415
|
+
ties: fits.slice(1).filter(same).map((c) => c.tag),
|
|
416
|
+
compared: index.tags.length,
|
|
417
|
+
identical: files.filter((p) => existing.files[p].class !== "generated"),
|
|
418
|
+
edited: [],
|
|
419
|
+
missing: [],
|
|
420
|
+
index: "computed",
|
|
421
|
+
lineage,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** One line per fact, for the terminal. */
|
|
426
|
+
export function describeAdoption(p: AdoptionResult): string[] {
|
|
427
|
+
const at = (path: string) => (p.scope === "." ? path : `${p.scope}/${path}`);
|
|
428
|
+
const c = p.chosen;
|
|
429
|
+
const lines = [`${p.scope} ${p.template}@${c.tag} (${c.commit.slice(0, 12)})`];
|
|
430
|
+
if (p.by === "lineage") {
|
|
431
|
+
const prev = p.lineage.adoption!.previous!;
|
|
432
|
+
lines.push(` moved from ${prev.template}: every one of the ${c.identical} recorded file(s) has the same content at ${c.tag}; ${p.compared} version(s) compared`);
|
|
433
|
+
} else {
|
|
434
|
+
lines.push(` matched ${c.identical} of ${c.files} template file(s); ${c.edited} edited, ${c.missing} not in the scope; ${p.compared} version(s) compared`);
|
|
435
|
+
}
|
|
436
|
+
if (p.ties.length > 0) {
|
|
437
|
+
const named = p.ties.length > 3 ? `${p.ties[0]} to ${p.ties[p.ties.length - 1]}` : p.ties.join(", ");
|
|
438
|
+
lines.push(` ${p.ties.length} later version(s) fit equally (${named}); the oldest is chosen. Name one with --from <repo>@<ref> if the scope came from it.`);
|
|
439
|
+
}
|
|
440
|
+
for (const a of p.alternatives) lines.push(` next: ${a.tag}, ${a.identical} of ${a.files} identical, ${a.edited} edited, ${a.missing} missing`);
|
|
441
|
+
for (const path of p.edited) lines.push(` edited: ${at(path)}`);
|
|
442
|
+
for (const path of p.missing) lines.push(` not in the scope: ${at(path)}`);
|
|
443
|
+
if (p.by === "files") {
|
|
444
|
+
lines.push(` index: ${p.index === "cache" ? "from the cache, re-checked at the chosen version" : "computed from the template"}`);
|
|
445
|
+
lines.push(
|
|
446
|
+
p.trust
|
|
447
|
+
? ` adopted range: up to ${p.trust.entry.to.slice(0, 12)}, added to ${p.trust.path}; it counts once that change is merged to the base branch`
|
|
448
|
+
: ` adopted range: up to ${p.lineage.adoption!.commits.to.slice(0, 12)}, already in ${TRUST_FILE}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return lines;
|
|
452
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]`:
|
|
2
|
+
* `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>] [--live --env <env>]`:
|
|
3
3
|
* the lineage checks (#2550, D9), and the declaration checks (#2535, D16)
|
|
4
4
|
* when a declaration sits between the current directory and the git root.
|
|
5
5
|
*
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* pipelines (#2542) and generated files (#2541) as well as the declaration
|
|
13
13
|
* itself (#2641). `--generated` runs each declared generator too. `--kind`
|
|
14
14
|
* reads a record kind's records and reports the files they pin by hash that
|
|
15
|
-
* have changed since (#2549).
|
|
15
|
+
* have changed since (#2549). `--live --env <env>` resolves each declared
|
|
16
|
+
* member link against the live graph of `<env>` (`./checks/live.ts`, #2549):
|
|
17
|
+
* it is the one option that reaches a network, through each member's own
|
|
18
|
+
* `chant graph --live`, and is catalogued as egress.
|
|
16
19
|
*
|
|
17
20
|
* `chant workspace upgrade` runs the same checks in its staging worktree
|
|
18
21
|
* before it reaches its gate.
|
|
@@ -88,7 +91,7 @@ export function findingKey(f: CheckFinding): string {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
const USAGE =
|
|
91
|
-
"chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]\n chant workspace check --changes <base>..<head> [--work <id>] [--severity off|warn|fail] [--kind <kind file>...] [--json]";
|
|
94
|
+
"chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>] [--live --env <env>]\n chant workspace check --changes <base>..<head> [--work <id>] [--severity off|warn|fail] [--kind <kind file>...] [--json]";
|
|
92
95
|
const FORMATS = ["stylish", "json", "sarif"] as const;
|
|
93
96
|
|
|
94
97
|
/** A lock finding as a lint diagnostic, for `--format json` and `--format sarif`. */
|
|
@@ -149,7 +152,7 @@ export type CheckDocument =
|
|
|
149
152
|
* to `cwd`. `runGenerators` is `--generated`; with `at` it does nothing,
|
|
150
153
|
* since the member facts describe the working tree, not the revision.
|
|
151
154
|
*/
|
|
152
|
-
export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean; kind?: string } = {}): Promise<CheckDocument> {
|
|
155
|
+
export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean; kind?: string; live?: { env: string; onStderr?: (text: string) => void } } = {}): Promise<CheckDocument> {
|
|
153
156
|
// Loaded here, not at the top: `workspace upgrade` imports this module for checkLineage alone.
|
|
154
157
|
const [{ findDeclarationDir, readDeclaration, readerVersion }, { gitTop, gitTree, resolveCommit, workingTree }] = await Promise.all([
|
|
155
158
|
import("./declaration"),
|
|
@@ -196,6 +199,7 @@ export async function runChecks(cwd: string, at?: string, options: { runGenerato
|
|
|
196
199
|
runGenerators: options.runGenerators === true,
|
|
197
200
|
...(commit !== null ? { tree } : {}),
|
|
198
201
|
...(records ? { records } : {}),
|
|
202
|
+
...(options.live ? { live: options.live } : {}),
|
|
199
203
|
});
|
|
200
204
|
let name: string | null = null;
|
|
201
205
|
try {
|
|
@@ -238,6 +242,19 @@ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
|
|
|
238
242
|
if (handed !== undefined) return handed;
|
|
239
243
|
return (await import("./changes-cli")).runWorkspaceChanges(ctx, root);
|
|
240
244
|
}
|
|
245
|
+
if (ctx.args.live) {
|
|
246
|
+
// A live read is of the account now, and it needs the environment each member's `chant graph --live` reads.
|
|
247
|
+
const problem =
|
|
248
|
+
ctx.args.at !== undefined
|
|
249
|
+
? "--live reads the account as it stands now, and --at reads the source at a revision; chant workspace check takes one of them"
|
|
250
|
+
: !ctx.args.env
|
|
251
|
+
? "--live needs an environment, as each member's chant graph --live does: --live --env <env>"
|
|
252
|
+
: undefined;
|
|
253
|
+
if (problem) {
|
|
254
|
+
console.error(formatError({ message: problem, hint: USAGE }));
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
241
258
|
const format = (ctx.args.format || "stylish") as (typeof FORMATS)[number];
|
|
242
259
|
if (!FORMATS.includes(format)) {
|
|
243
260
|
console.error(formatError({ message: `--format ${format} is not a check format; use stylish, json or sarif`, hint: USAGE }));
|
|
@@ -250,12 +267,16 @@ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
|
|
|
250
267
|
const handed = await handToRootChant(root, ctx.args.at);
|
|
251
268
|
if (handed !== undefined) return handed;
|
|
252
269
|
}
|
|
253
|
-
const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true, ...(ctx.args.kind !== undefined ? { kind: ctx.args.kind } : {}) });
|
|
270
|
+
const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true, ...(ctx.args.kind !== undefined ? { kind: ctx.args.kind } : {}), ...(ctx.args.live && ctx.args.env ? { live: { env: ctx.args.env, onStderr: (text: string) => process.stderr.write(text) } } : {}) });
|
|
254
271
|
if ("error" in doc) {
|
|
255
272
|
if (ctx.args.json || format === "json") console.log(JSON.stringify(doc, null, 2));
|
|
256
273
|
else console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
|
|
257
274
|
return 1;
|
|
258
275
|
}
|
|
276
|
+
if (ctx.args.live && !doc.declaration) {
|
|
277
|
+
console.error(formatError({ message: "--live resolves a workspace's member links, and there is no chant.workspace.json between here and the git root", hint: USAGE }));
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
259
280
|
const { declaration, ok } = doc;
|
|
260
281
|
const report = { lock: doc.lock, findings: doc.findings };
|
|
261
282
|
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { formatError, formatSuccess } from "../cli/format";
|
|
13
13
|
import type { CommandContext } from "../cli/registry";
|
|
14
|
-
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type LineageSource, type ManualStep } from "./lineage-lock";
|
|
14
|
+
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type Adoption, type LineageSource, type ManualStep } from "./lineage-lock";
|
|
15
|
+
import { lineageProvenance, type LineageProvenance } from "./lineage-provenance";
|
|
15
16
|
|
|
16
17
|
const USAGE = "chant workspace lineage [--json] | chant workspace lineage resolve <path>";
|
|
17
18
|
|
|
@@ -28,6 +29,10 @@ export interface LineageScopeView {
|
|
|
28
29
|
edited: string[];
|
|
29
30
|
missing: string[];
|
|
30
31
|
manualSteps: ManualStep[];
|
|
32
|
+
/** How adopt-lineage recorded the lineage (#2551), when it did. */
|
|
33
|
+
adoption?: Adoption;
|
|
34
|
+
/** For a lineage adopted by its files: whether the policy at base admits the adopted range (D5, P7). */
|
|
35
|
+
provenance?: LineageProvenance;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/** One line naming a scope's source kind and where it points (#2647). */
|
|
@@ -52,7 +57,7 @@ export function lineageView(root: string): { lock: string; scopes: LineageScopeV
|
|
|
52
57
|
if (!lock) return null;
|
|
53
58
|
const scopes = Object.entries(lock.scopes).map(([scope, lineage]) => {
|
|
54
59
|
const st = scopeStatus(root, scope, lineage);
|
|
55
|
-
|
|
60
|
+
const view: LineageScopeView = {
|
|
56
61
|
scope,
|
|
57
62
|
kind: lineage.kind,
|
|
58
63
|
...(lineage.name !== undefined ? { name: lineage.name } : {}),
|
|
@@ -64,7 +69,10 @@ export function lineageView(root: string): { lock: string; scopes: LineageScopeV
|
|
|
64
69
|
edited: st.customised,
|
|
65
70
|
missing: st.missing,
|
|
66
71
|
manualSteps: lineage.manualSteps,
|
|
72
|
+
...(lineage.adoption ? { adoption: lineage.adoption } : {}),
|
|
67
73
|
};
|
|
74
|
+
const provenance = lineageProvenance(root, lineage);
|
|
75
|
+
return provenance ? { ...view, provenance } : view;
|
|
68
76
|
});
|
|
69
77
|
return { lock: LOCK_FILE, scopes };
|
|
70
78
|
}
|
|
@@ -93,6 +101,8 @@ export async function runWorkspaceLineage(ctx: CommandContext): Promise<number>
|
|
|
93
101
|
const at = typeof s.address?.commit === "string" ? ` (${s.address.commit.slice(0, 12)})` : "";
|
|
94
102
|
lines.push(`${s.scope} ${s.kind}${s.name ? ` ${s.name}` : ""} ${s.template}${pin}${at}`);
|
|
95
103
|
lines.push(` source: ${describeSource(s.source)}`);
|
|
104
|
+
if (s.adoption?.by === "lineage" && s.adoption.previous) lines.push(` moved from ${describeSource(s.adoption.previous.source)} by adopt-lineage`);
|
|
105
|
+
if (s.adoption?.by === "files") lines.push(` adopted up to ${s.adoption.commits.to.slice(0, 12)}: ${s.provenance?.level ?? "unattested"}, ${s.provenance?.reason ?? ""}`.replace(/, $/, ""));
|
|
96
106
|
lines.push(` ${s.files} file(s), ${s.edited.length} edited, ${s.missing.length} deleted`);
|
|
97
107
|
for (const m of s.manualSteps) lines.push(` manual step: ${s.scope === "." ? "" : `${s.scope}/`}${m.path} (${m.reason})`);
|
|
98
108
|
}
|