filegrc 0.7.1 → 0.9.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 +7 -7
- package/model/index.js +22 -4
- package/model/v5.json +10233 -0
- package/model/v6.json +10358 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +182 -45
- package/src/audit-transition.js +3 -2
- package/src/batch-review.js +7 -6
- package/src/cli.js +143 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +181 -0
- package/src/evidence-packet.js +199 -78
- package/src/external-reviewer.js +5 -4
- package/src/files.js +173 -35
- package/src/git.js +71 -7
- package/src/index.js +11 -1
- package/src/model-migration.js +363 -7
- package/src/obligations.js +14 -11
- package/src/policy-library.js +7 -3
- package/src/program-lifecycle.js +131 -3
- package/src/program-path.js +19 -13
- package/src/program-readiness.js +338 -71
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +36 -7
- package/src/setup.js +8 -5
- package/src/soc2.js +3 -2
- package/src/validate.js +178 -16
- package/src/web.js +264 -17
- package/src/workflow.js +38 -7
- package/src/workspace.js +5 -0
package/src/files.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { constants, link, lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { getResourceDefinition, loadModel } from "../model/index.js";
|
|
4
|
+
import { getResourceDefinition, loadModel, modelSupports } from "../model/index.js";
|
|
5
5
|
import { openPlaceholderCount } from "./content-readiness.js";
|
|
6
6
|
import { serializeWorkspaceMutation, workspaceValidationDeferred } from "./mutation.js";
|
|
7
7
|
import { isCanonicalDataPath, resolveDataPath, resolveWorkspaceRoot } from "./paths.js";
|
|
8
|
+
import { documentIsAuditSpecific } from "./program-lifecycle.js";
|
|
8
9
|
import { markdownEntries } from "./resource-markdown.js";
|
|
9
10
|
import { measureTiming } from "./timing.js";
|
|
10
11
|
import { loadWorkspace } from "./workspace.js";
|
|
@@ -204,7 +205,25 @@ export async function applyResourceBatch(input, changes) {
|
|
|
204
205
|
return serializeWorkspaceMutation(input, (root) => applyResourceBatchUnlocked(root, changes));
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
async function
|
|
208
|
+
export async function applyDocumentActivationBatch(input, changes) {
|
|
209
|
+
return serializeWorkspaceMutation(input, (root) => (
|
|
210
|
+
applyResourceBatchUnlocked(root, changes, "document-activation")
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function applyGovernedContentActivationBatch(input, changes) {
|
|
215
|
+
return serializeWorkspaceMutation(input, (root) => (
|
|
216
|
+
applyResourceBatchUnlocked(root, changes, "governed-content-activation")
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function applyModelMigrationBatch(input, changes) {
|
|
221
|
+
return serializeWorkspaceMutation(input, (root) => (
|
|
222
|
+
applyResourceBatchUnlocked(root, changes, "model-migration")
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function applyResourceBatchUnlocked(input, changes = {}, lifecycleOperation = null) {
|
|
208
227
|
const creates = changes.create || [];
|
|
209
228
|
const updates = changes.update || [];
|
|
210
229
|
const moves = changes.movePaths || [];
|
|
@@ -253,12 +272,31 @@ async function applyResourceBatchUnlocked(input, changes = {}) {
|
|
|
253
272
|
"A cross-model resource batch must validate the whole workspace and update its dataModelVersion to the target model."
|
|
254
273
|
);
|
|
255
274
|
}
|
|
275
|
+
if (lifecycleOperation === "model-migration" && !targetModelVersion) {
|
|
276
|
+
throw new Error("The model-migration lifecycle operation requires a cross-model resource batch.");
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
["document-activation", "governed-content-activation"].includes(lifecycleOperation)
|
|
280
|
+
&& (targetModelVersion || changes.validateWholeWorkspace !== true)
|
|
281
|
+
) {
|
|
282
|
+
throw new Error("The governed-content activation lifecycle operation requires same-model whole-workspace validation.");
|
|
283
|
+
}
|
|
256
284
|
const writeModel = targetModelVersion ? loadModel(targetModelVersion) : loaded.model;
|
|
257
285
|
const deferValidation = workspaceValidationDeferred();
|
|
258
286
|
const before = deferValidation || changes.validateWholeWorkspace
|
|
259
287
|
? null
|
|
260
288
|
: await validateWorkspace(loaded);
|
|
261
289
|
const existingById = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
290
|
+
for (const record of updates) {
|
|
291
|
+
if (!record || Array.isArray(record) || typeof record !== "object" || typeof record.id !== "string") continue;
|
|
292
|
+
const existing = existingById.get(record.id);
|
|
293
|
+
if (!existing) continue;
|
|
294
|
+
assertRevision(
|
|
295
|
+
existing.source,
|
|
296
|
+
expectedRevisions[record.id] || existing.revision,
|
|
297
|
+
`Resource "${record.id}"`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
262
300
|
const ids = new Set();
|
|
263
301
|
const writes = [];
|
|
264
302
|
const contentWrites = [];
|
|
@@ -315,13 +353,14 @@ async function applyResourceBatchUnlocked(input, changes = {}) {
|
|
|
315
353
|
const nextRecord = hasContentUpdate
|
|
316
354
|
? await prepareApprovalBinding(loaded, record, recordContentWrites, existing.record)
|
|
317
355
|
: record;
|
|
356
|
+
assertGovernedContentLifecycleMutation(existing.record, nextRecord, loaded.model, lifecycleOperation);
|
|
318
357
|
writes.push({ operation: path === previousPath ? "update" : "move-update", path, previousPath, record: nextRecord, previous, fileMode: mode });
|
|
319
358
|
}
|
|
320
359
|
for (const resourceId of Object.keys(contentUpdates)) {
|
|
321
360
|
if (preparedContentIds.has(resourceId)) continue;
|
|
322
361
|
const existing = existingById.get(resourceId);
|
|
323
362
|
if (!existing) throw new Error(`Resource "${resourceId}" was not found.`);
|
|
324
|
-
if (approvalBound(existing.record)) {
|
|
363
|
+
if (approvalBound(existing.record, loaded.model)) {
|
|
325
364
|
throw new Error(`Batch content for approved or active resource "${resourceId}" needs a matching resource update and validation of its approval binding.`);
|
|
326
365
|
}
|
|
327
366
|
contentWrites.push(...await prepareContentWrites(loaded, existing.record, contentUpdates[resourceId], {
|
|
@@ -574,6 +613,7 @@ async function updateResourceUnlocked(input, type, id, record, options) {
|
|
|
574
613
|
});
|
|
575
614
|
const existing = loaded.entries.find(({ record: candidate }) => candidate.id === id)?.record;
|
|
576
615
|
const nextRecord = await prepareApprovalBinding(loaded, record, contentWrites, existing);
|
|
616
|
+
assertGovernedContentLifecycleMutation(existing, nextRecord, loaded.model, null);
|
|
577
617
|
try {
|
|
578
618
|
for (const item of contentWrites) await writeTextAtomic(item.path, item.source);
|
|
579
619
|
await writeAtomic(path, nextRecord);
|
|
@@ -783,39 +823,105 @@ async function prepareApprovalBinding(loaded, record, contentWrites, previousRec
|
|
|
783
823
|
if (record.type === "attestation") {
|
|
784
824
|
return prepareAttestationBinding(loaded, record, previousRecord);
|
|
785
825
|
}
|
|
786
|
-
const
|
|
787
|
-
if (!
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
826
|
+
const bindingFields = contentBindingFields(record, loaded.model);
|
|
827
|
+
if (!bindingFields.length) return record;
|
|
828
|
+
if (
|
|
829
|
+
(
|
|
830
|
+
record.type === "document" && modelSupports(loaded.model, "governed-document-activation")
|
|
831
|
+
|| record.type === "training" && modelSupports(loaded.model, "governed-training-activation")
|
|
832
|
+
)
|
|
833
|
+
&& record.status === "active"
|
|
834
|
+
&& previousRecord?.status !== "active"
|
|
835
|
+
) {
|
|
836
|
+
const step = record.type === "document" && documentIsAuditSpecific(record, loaded.model) ? "Step 5" : "Step 3";
|
|
837
|
+
const title = getResourceDefinition(loaded.model, record.type).title;
|
|
838
|
+
throw new Error(`${title} "${record.id}" must use the dedicated ${step} ${title} activation operation after approval.`);
|
|
796
839
|
}
|
|
840
|
+
const nextRecord = structuredClone(record);
|
|
797
841
|
const proposed = new Map(contentWrites.map((item) => [item.dataRelativePath, item.source]));
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
throw error;
|
|
807
|
-
}
|
|
842
|
+
for (const { field, bound, label } of bindingFields) {
|
|
843
|
+
if (!bound(record)) {
|
|
844
|
+
delete nextRecord[field];
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (bound(previousRecord) && previousRecord[field]) {
|
|
848
|
+
nextRecord[field] = structuredClone(previousRecord[field]);
|
|
849
|
+
continue;
|
|
808
850
|
}
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
851
|
+
const revisions = {};
|
|
852
|
+
for (const item of markdownEntries(loaded.model, nextRecord)) {
|
|
853
|
+
let source = proposed.get(item.path);
|
|
854
|
+
if (source === undefined) {
|
|
855
|
+
try {
|
|
856
|
+
source = await readFile(resolveDataPath(loaded.root, item.path), "utf8");
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (error.code === "ENOENT") continue;
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const placeholders = openPlaceholderCount(source);
|
|
863
|
+
if (placeholders) {
|
|
864
|
+
throw new Error(`Cannot ${label} ${record.title} while its ${item.label} Markdown contains ${placeholders} open ${placeholders === 1 ? "placeholder" : "placeholders"}. Complete the facts and review the exact content first.`);
|
|
865
|
+
}
|
|
866
|
+
revisions[item.path] = contentRevision(source);
|
|
812
867
|
}
|
|
813
|
-
|
|
868
|
+
nextRecord[field] = revisions;
|
|
814
869
|
}
|
|
815
|
-
nextRecord[bindingField] = revisions;
|
|
816
870
|
return nextRecord;
|
|
817
871
|
}
|
|
818
872
|
|
|
873
|
+
function assertGovernedContentLifecycleMutation(previousRecord, nextRecord, model, lifecycleOperation) {
|
|
874
|
+
if (lifecycleOperation === "model-migration") return;
|
|
875
|
+
const governedTraining = previousRecord?.type === "training"
|
|
876
|
+
&& nextRecord?.type === "training"
|
|
877
|
+
&& modelSupports(model, "governed-training-activation");
|
|
878
|
+
const governedDocument = previousRecord?.type === "document"
|
|
879
|
+
&& nextRecord?.type === "document"
|
|
880
|
+
&& modelSupports(model, "governed-document-activation");
|
|
881
|
+
if (
|
|
882
|
+
!previousRecord
|
|
883
|
+
|| (!governedDocument && !governedTraining)
|
|
884
|
+
) return;
|
|
885
|
+
const title = getResourceDefinition(model, nextRecord.type).title;
|
|
886
|
+
const approvedStatuses = new Set(["approved", "active", "superseded", "retired"]);
|
|
887
|
+
const activatedStatuses = new Set(["active", "superseded", "retired"]);
|
|
888
|
+
if (approvedStatuses.has(previousRecord.status) && approvedStatuses.has(nextRecord.status)) {
|
|
889
|
+
assertLifecycleFieldsUnchanged(previousRecord, nextRecord, [
|
|
890
|
+
"approverIds",
|
|
891
|
+
"approvedOn",
|
|
892
|
+
"approvedContentRevisions"
|
|
893
|
+
], "approval", title);
|
|
894
|
+
}
|
|
895
|
+
if (activatedStatuses.has(previousRecord.status) && activatedStatuses.has(nextRecord.status)) {
|
|
896
|
+
assertLifecycleFieldsUnchanged(previousRecord, nextRecord, [
|
|
897
|
+
"activationBasis",
|
|
898
|
+
"activatedByIds",
|
|
899
|
+
"activatedOn",
|
|
900
|
+
"activatedContentRevisions",
|
|
901
|
+
"effectiveOn"
|
|
902
|
+
], "activation", title);
|
|
903
|
+
}
|
|
904
|
+
if (
|
|
905
|
+
nextRecord.status === "active"
|
|
906
|
+
&& previousRecord.status !== "active"
|
|
907
|
+
&& !["document-activation", "governed-content-activation"].includes(lifecycleOperation)
|
|
908
|
+
) {
|
|
909
|
+
const step = governedDocument && documentIsAuditSpecific(nextRecord, model) ? "Step 5" : "Step 3";
|
|
910
|
+
throw new Error(`${title} "${nextRecord.id}" must use the dedicated ${step} ${title} activation operation after approval.`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function assertLifecycleFieldsUnchanged(previousRecord, nextRecord, fields, eventLabel, resourceTitle) {
|
|
915
|
+
const changed = fields.filter((field) => (
|
|
916
|
+
JSON.stringify(previousRecord[field] ?? null) !== JSON.stringify(nextRecord[field] ?? null)
|
|
917
|
+
));
|
|
918
|
+
if (!changed.length) return;
|
|
919
|
+
throw new Error(
|
|
920
|
+
`${resourceTitle} "${nextRecord.id}" ${eventLabel} facts are immutable after the event: ${changed.join(", ")}. `
|
|
921
|
+
+ `Move the ${resourceTitle} back to ${eventLabel === "approval" ? "draft" : "approved"} and record a new lifecycle event.`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
819
925
|
async function prepareAttestationBinding(loaded, record, previousRecord = null) {
|
|
820
926
|
const nextRecord = structuredClone(record);
|
|
821
927
|
const bound = record.status === "completed" && record.attestationMethod === "git-approval";
|
|
@@ -848,22 +954,54 @@ async function prepareAttestationBinding(loaded, record, previousRecord = null)
|
|
|
848
954
|
return nextRecord;
|
|
849
955
|
}
|
|
850
956
|
|
|
851
|
-
function approvalBound(record) {
|
|
957
|
+
function approvalBound(record, model) {
|
|
852
958
|
if (!record || !["policy", "document", "training"].includes(record.type)) return false;
|
|
853
959
|
const statuses = record.type === "policy"
|
|
854
960
|
? ["approved", "active", "superseded", "retired"]
|
|
855
961
|
: record.type === "document"
|
|
856
|
-
? ["active", "superseded", "retired"]
|
|
857
|
-
:
|
|
962
|
+
? ["approved", "active", "superseded", "retired"]
|
|
963
|
+
: modelSupports(model || 0, "governed-training-activation")
|
|
964
|
+
? ["approved", "active", "superseded", "retired"]
|
|
965
|
+
: ["active", "retired"];
|
|
858
966
|
return statuses.includes(record.status);
|
|
859
967
|
}
|
|
860
968
|
|
|
861
|
-
function
|
|
862
|
-
|
|
969
|
+
function contentBindingFields(record, model) {
|
|
970
|
+
const fields = [];
|
|
971
|
+
if (["policy", "document"].includes(record?.type)) {
|
|
972
|
+
fields.push({
|
|
973
|
+
field: "approvedContentRevisions",
|
|
974
|
+
bound: (candidate) => approvalBound(candidate, model),
|
|
975
|
+
label: record.type === "policy" ? "approve or activate" : "approve"
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
if (record?.type === "document" && model.resources.document?.fields?.activatedContentRevisions) {
|
|
979
|
+
fields.push({
|
|
980
|
+
field: "activatedContentRevisions",
|
|
981
|
+
bound: (candidate) => (
|
|
982
|
+
["active", "superseded", "retired"].includes(candidate?.status)
|
|
983
|
+
&& candidate.activationBasis === "recorded"
|
|
984
|
+
),
|
|
985
|
+
label: "activate"
|
|
986
|
+
});
|
|
987
|
+
}
|
|
863
988
|
if (record?.type === "training" && model.resources.training?.fields?.effectiveContentRevisions) {
|
|
864
|
-
|
|
989
|
+
fields.push({ field: "effectiveContentRevisions", bound: (candidate) => approvalBound(candidate, model), label: "approve or activate" });
|
|
990
|
+
}
|
|
991
|
+
if (record?.type === "training" && model.resources.training?.fields?.approvedContentRevisions) {
|
|
992
|
+
fields.push({ field: "approvedContentRevisions", bound: (candidate) => approvalBound(candidate, model), label: "approve" });
|
|
993
|
+
}
|
|
994
|
+
if (record?.type === "training" && model.resources.training?.fields?.activatedContentRevisions) {
|
|
995
|
+
fields.push({
|
|
996
|
+
field: "activatedContentRevisions",
|
|
997
|
+
bound: (candidate) => (
|
|
998
|
+
["active", "superseded", "retired"].includes(candidate?.status)
|
|
999
|
+
&& candidate.activationBasis === "recorded"
|
|
1000
|
+
),
|
|
1001
|
+
label: "activate"
|
|
1002
|
+
});
|
|
865
1003
|
}
|
|
866
|
-
return
|
|
1004
|
+
return fields;
|
|
867
1005
|
}
|
|
868
1006
|
|
|
869
1007
|
async function exclusiveContentFiles(loaded, record) {
|
package/src/git.js
CHANGED
|
@@ -133,29 +133,88 @@ export function getWorkspaceHistories(input, relativePaths, limitPerFile = 12, o
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
export function getFileAtRevision(input, revision, relativePath) {
|
|
136
|
+
return getFilesAtRevisions(input, [{ revision, relativePath }])[0];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getFilesAtRevisions(input, requests) {
|
|
136
140
|
const root = resolveWorkspaceRoot(input);
|
|
137
|
-
|
|
141
|
+
const invalid = Array.isArray(requests) && requests.find(({ revision, relativePath } = {}) => (
|
|
142
|
+
!/^[a-f0-9]{40}$/i.test(String(revision)) || !isSafeDataGitPath(relativePath)
|
|
143
|
+
));
|
|
144
|
+
if (!Array.isArray(requests) || invalid) {
|
|
138
145
|
throw new Error("Historical file exports require a Git commit and a data/ path.");
|
|
139
146
|
}
|
|
147
|
+
if (!requests.length) return [];
|
|
140
148
|
try {
|
|
141
149
|
const topLevel = git(root, ["rev-parse", "--show-toplevel"]);
|
|
142
150
|
const workspacePrefix = relative(topLevel, root).split(sep).join("/");
|
|
143
|
-
if (workspacePrefix === ".." || workspacePrefix.startsWith("../")) return null;
|
|
144
|
-
const
|
|
145
|
-
|
|
151
|
+
if (workspacePrefix === ".." || workspacePrefix.startsWith("../")) return requests.map(() => null);
|
|
152
|
+
const results = [];
|
|
153
|
+
for (let offset = 0; offset < requests.length; offset += 4) {
|
|
154
|
+
const batch = requests.slice(offset, offset + 4);
|
|
155
|
+
const specifications = batch.map(({ revision, relativePath }) => {
|
|
156
|
+
const repositoryPath = workspacePrefix ? `${workspacePrefix}/${relativePath}` : relativePath;
|
|
157
|
+
return `${revision}:${repositoryPath}`;
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
const output = measureTimingSync("git-history-export", () => execFileSync("git", ["cat-file", "--batch"], {
|
|
161
|
+
cwd: root,
|
|
162
|
+
input: `${specifications.join("\n")}\n`,
|
|
163
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
164
|
+
timeout: 10_000,
|
|
165
|
+
maxBuffer: 80_000_000
|
|
166
|
+
}));
|
|
167
|
+
results.push(...parseBatchObjects(output, batch.length));
|
|
168
|
+
} catch {
|
|
169
|
+
results.push(...specifications.map((specification) => readHistoricalFile(root, specification)));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
} catch {
|
|
174
|
+
return requests.map(() => null);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readHistoricalFile(root, specification) {
|
|
179
|
+
try {
|
|
180
|
+
return measureTimingSync("git-history-export", () => execFileSync("git", ["show", specification], {
|
|
146
181
|
cwd: root,
|
|
147
182
|
encoding: "utf8",
|
|
148
183
|
stdio: ["ignore", "pipe", "ignore"],
|
|
149
184
|
timeout: 10_000,
|
|
150
185
|
maxBuffer: 20_000_000
|
|
151
|
-
});
|
|
186
|
+
}));
|
|
152
187
|
} catch {
|
|
153
188
|
return null;
|
|
154
189
|
}
|
|
155
190
|
}
|
|
156
191
|
|
|
192
|
+
function parseBatchObjects(output, expected) {
|
|
193
|
+
const results = [];
|
|
194
|
+
let offset = 0;
|
|
195
|
+
for (let index = 0; index < expected; index += 1) {
|
|
196
|
+
const headerEnd = output.indexOf(10, offset);
|
|
197
|
+
if (headerEnd < 0) throw new Error("Git returned an incomplete historical object header.");
|
|
198
|
+
const header = output.subarray(offset, headerEnd).toString("utf8");
|
|
199
|
+
offset = headerEnd + 1;
|
|
200
|
+
if (header.endsWith(" missing")) {
|
|
201
|
+
results.push(null);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const size = Number(header.split(" ").at(-1));
|
|
205
|
+
if (!Number.isSafeInteger(size) || size < 0 || offset + size >= output.length) {
|
|
206
|
+
throw new Error("Git returned an invalid historical object size.");
|
|
207
|
+
}
|
|
208
|
+
results.push(output.subarray(offset, offset + size).toString("utf8"));
|
|
209
|
+
offset += size;
|
|
210
|
+
if (output[offset++] !== 10) throw new Error("Git returned an incomplete historical object.");
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
|
|
157
215
|
function isSafeDataGitPath(value) {
|
|
158
216
|
return isCanonicalDataPath(value)
|
|
217
|
+
&& !/[\r\n]/.test(value)
|
|
159
218
|
&& value.startsWith("data/")
|
|
160
219
|
&& value !== "data/";
|
|
161
220
|
}
|
|
@@ -230,6 +289,7 @@ export async function getWorkspaceRevisionSnapshot(input = process.cwd()) {
|
|
|
230
289
|
available: true,
|
|
231
290
|
commit: parsed.commit,
|
|
232
291
|
shortCommit: parsed.commit?.slice(0, 8) ?? "no commits",
|
|
292
|
+
branch: parsed.branch,
|
|
233
293
|
clean: parsed.changePaths.length === 0,
|
|
234
294
|
changes: parsed.changePaths,
|
|
235
295
|
workspaceChangePaths: parsed.changePaths
|
|
@@ -1121,11 +1181,14 @@ function parsePorcelainV2(source, topLevel, root) {
|
|
|
1121
1181
|
function parseWorkspaceRevision(source) {
|
|
1122
1182
|
const fields = source.split("\0").filter(Boolean);
|
|
1123
1183
|
let commit = null;
|
|
1184
|
+
let branch = null;
|
|
1124
1185
|
const changePaths = [];
|
|
1125
1186
|
for (let index = 0; index < fields.length; index += 1) {
|
|
1126
1187
|
const field = fields[index];
|
|
1127
1188
|
if (field.startsWith("# branch.oid ")) {
|
|
1128
1189
|
commit = field.slice(13) === "(initial)" ? null : field.slice(13);
|
|
1190
|
+
} else if (field.startsWith("# branch.head ")) {
|
|
1191
|
+
branch = field.slice(14) === "(detached)" ? null : field.slice(14);
|
|
1129
1192
|
} else if (/^[12u?!] /.test(field)) {
|
|
1130
1193
|
changePaths.push(porcelainV2Path(field));
|
|
1131
1194
|
if (field.startsWith("2 ")) {
|
|
@@ -1135,6 +1198,7 @@ function parseWorkspaceRevision(source) {
|
|
|
1135
1198
|
}
|
|
1136
1199
|
return {
|
|
1137
1200
|
commit,
|
|
1201
|
+
branch,
|
|
1138
1202
|
changePaths: [...new Set(changePaths.filter(Boolean))].sort()
|
|
1139
1203
|
};
|
|
1140
1204
|
}
|
|
@@ -1290,13 +1354,13 @@ async function tryGitAsync(cwd, args, operation) {
|
|
|
1290
1354
|
}
|
|
1291
1355
|
|
|
1292
1356
|
function git(cwd, args) {
|
|
1293
|
-
return execFileSync("git", args, {
|
|
1357
|
+
return measureTimingSync("git-command-sync", () => execFileSync("git", args, {
|
|
1294
1358
|
cwd,
|
|
1295
1359
|
encoding: "utf8",
|
|
1296
1360
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1297
1361
|
timeout: 10_000,
|
|
1298
1362
|
maxBuffer: 20_000_000
|
|
1299
|
-
}).trim();
|
|
1363
|
+
}).trim());
|
|
1300
1364
|
}
|
|
1301
1365
|
|
|
1302
1366
|
function tryGit(cwd, args) {
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,12 @@ export {
|
|
|
2
2
|
ACTIVE_MODEL_VERSION,
|
|
3
3
|
getResourceDefinition,
|
|
4
4
|
loadModel,
|
|
5
|
+
MODEL_CAPABILITY_VERSIONS,
|
|
6
|
+
modelSupports,
|
|
5
7
|
SUPPORTED_MODEL_VERSIONS
|
|
6
8
|
} from "../model/index.js";
|
|
7
9
|
export { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
|
|
8
|
-
export { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
10
|
+
export { assessAuditDocumentActivations, assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
9
11
|
export { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
10
12
|
export {
|
|
11
13
|
applyApplicabilityReview,
|
|
@@ -52,6 +54,14 @@ export { generateModelDocumentation } from "./model-docs.js";
|
|
|
52
54
|
export { renderMarkdown } from "./markdown.js";
|
|
53
55
|
export { migrateModel, planModelMigration } from "./model-migration.js";
|
|
54
56
|
export { activatePolicies, planPolicyActivation, scaffoldPolicyActivation } from "./policy-activation.js";
|
|
57
|
+
export {
|
|
58
|
+
activateDocuments,
|
|
59
|
+
activateGovernedContent,
|
|
60
|
+
planDocumentActivation,
|
|
61
|
+
planGovernedContentActivation,
|
|
62
|
+
scaffoldDocumentActivation,
|
|
63
|
+
scaffoldGovernedContentActivation
|
|
64
|
+
} from "./document-activation.js";
|
|
55
65
|
export {
|
|
56
66
|
applyPolicyLibraryUpgrade,
|
|
57
67
|
assessPolicyLibraryUpgrades,
|