forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
|
@@ -29,7 +29,10 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
29
29
|
backlog: Object.freeze(['open', 'cancelled']),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// `reclaimable` is the historical row left behind when an expired active lease is
|
|
33
|
+
// superseded. It is distinct from `expired`: both are non-authoritative history,
|
|
34
|
+
// but reclaimable records that the slot was deliberately freed for another owner.
|
|
35
|
+
const CLAIM_STATES = Object.freeze(['active', 'released', 'expired', 'reclaimable']);
|
|
33
36
|
|
|
34
37
|
const DEFAULT_PRIORITY_RANK = 2;
|
|
35
38
|
const MAX_DISPLAY_PRIORITY_RANK = 4;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* global ActiveXObject, Enumerator, VBArray, WScript */
|
|
4
|
+
|
|
5
|
+
// This file is executed by Windows Script Host and loaded by Bun/Node tests.
|
|
6
|
+
// Keep its syntax compatible with the system JScript engine.
|
|
7
|
+
var ADS_PATH_FILE = 1;
|
|
8
|
+
var ADS_SD_FORMAT_IID = 1;
|
|
9
|
+
var ADS_SD_FORMAT_RAW = 2;
|
|
10
|
+
var ADS_SECURITY_INFO_OWNER = 1;
|
|
11
|
+
var ADS_SECURITY_INFO_DACL = 4;
|
|
12
|
+
var PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000;
|
|
13
|
+
var SE_DACL_PRESENT = 0x0004;
|
|
14
|
+
var SE_DACL_PROTECTED = 0x1000;
|
|
15
|
+
var SE_SELF_RELATIVE = 0x8000;
|
|
16
|
+
var FILE_ALL_ACCESS = 0x001f01ff;
|
|
17
|
+
var ACCESS_ALLOWED_ACE_TYPE = 0;
|
|
18
|
+
var OBJECT_INHERIT_ACE = 1;
|
|
19
|
+
var CONTAINER_INHERIT_ACE = 2;
|
|
20
|
+
var MAX_TARGETS = 128;
|
|
21
|
+
|
|
22
|
+
function invariant(condition, message) {
|
|
23
|
+
if (!condition) throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function atStage(code, action) {
|
|
27
|
+
try {
|
|
28
|
+
return action();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
error.forgeExitCode = code;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function bytesFromSafeArray(value) {
|
|
36
|
+
var values = new VBArray(value).toArray();
|
|
37
|
+
var bytes = [];
|
|
38
|
+
for (var i = 0; i < values.length; i++) bytes.push(values[i] & 0xff);
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function u16(bytes, offset) {
|
|
43
|
+
invariant(offset >= 0 && offset + 2 <= bytes.length, 'truncated uint16');
|
|
44
|
+
return bytes[offset] | (bytes[offset + 1] << 8);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function u32(bytes, offset) {
|
|
48
|
+
invariant(offset >= 0 && offset + 4 <= bytes.length, 'truncated uint32');
|
|
49
|
+
return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sidAt(bytes, offset) {
|
|
53
|
+
invariant(offset > 0 && offset + 8 <= bytes.length, 'missing SID');
|
|
54
|
+
var revision = bytes[offset];
|
|
55
|
+
var count = bytes[offset + 1];
|
|
56
|
+
var end = offset + 8 + (count * 4);
|
|
57
|
+
invariant(revision === 1 && end <= bytes.length, 'malformed SID');
|
|
58
|
+
var authority = 0;
|
|
59
|
+
for (var i = 0; i < 6; i++) authority = (authority * 256) + bytes[offset + 2 + i];
|
|
60
|
+
invariant(authority <= 9007199254740991, 'unsupported SID authority');
|
|
61
|
+
var parts = ['S', String(revision), String(authority)];
|
|
62
|
+
for (var j = 0; j < count; j++) parts.push(String(u32(bytes, offset + 8 + (j * 4))));
|
|
63
|
+
return { end: end, value: parts.join('-') };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inspectDescriptor(bytes, expectedOwnerSid, expectedAceFlags) {
|
|
67
|
+
invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
|
|
68
|
+
invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(expectedOwnerSid), 'invalid expected SID');
|
|
69
|
+
invariant(bytes[0] === 1, 'unsupported security descriptor revision');
|
|
70
|
+
var control = u16(bytes, 2);
|
|
71
|
+
invariant((control & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
|
|
72
|
+
invariant((control & SE_DACL_PRESENT) !== 0, 'DACL is absent');
|
|
73
|
+
invariant((control & SE_DACL_PROTECTED) !== 0, 'DACL is not protected');
|
|
74
|
+
var owner = sidAt(bytes, u32(bytes, 4));
|
|
75
|
+
invariant(owner.value === expectedOwnerSid, 'owner changed');
|
|
76
|
+
var daclOffset = u32(bytes, 16);
|
|
77
|
+
invariant(daclOffset > 0 && daclOffset + 8 <= bytes.length, 'DACL is malformed');
|
|
78
|
+
var aclRevision = bytes[daclOffset];
|
|
79
|
+
var aclSize = u16(bytes, daclOffset + 2);
|
|
80
|
+
var aceCount = u16(bytes, daclOffset + 4);
|
|
81
|
+
invariant(aclRevision === 2, 'unsupported ACL revision');
|
|
82
|
+
invariant(aclSize >= 8 && daclOffset + aclSize <= bytes.length, 'ACL size is malformed');
|
|
83
|
+
invariant(aceCount === 1, 'DACL must contain one ACE');
|
|
84
|
+
var aceOffset = daclOffset + 8;
|
|
85
|
+
invariant(aceOffset + 8 <= daclOffset + aclSize, 'ACE is truncated');
|
|
86
|
+
var aceType = bytes[aceOffset];
|
|
87
|
+
var aceFlags = bytes[aceOffset + 1];
|
|
88
|
+
var aceSize = u16(bytes, aceOffset + 2);
|
|
89
|
+
invariant(aceType === ACCESS_ALLOWED_ACE_TYPE, 'ACE must allow access');
|
|
90
|
+
invariant(aceFlags === expectedAceFlags, 'ACE flags are not exact');
|
|
91
|
+
invariant(aceSize >= 16 && aceOffset + aceSize === daclOffset + aclSize, 'ACE size is not exact');
|
|
92
|
+
invariant(u32(bytes, aceOffset + 4) === FILE_ALL_ACCESS, 'ACE is not full control');
|
|
93
|
+
var trustee = sidAt(bytes, aceOffset + 8);
|
|
94
|
+
invariant(trustee.value === expectedOwnerSid, 'ACE trustee is not the owner');
|
|
95
|
+
invariant(trustee.end === aceOffset + aceSize, 'ACE contains trailing data');
|
|
96
|
+
return { aceFlags: aceFlags, ownerSid: owner.value };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inspectIidDescriptor(descriptor, expectedOwner, expectedAceFlags, enumeratorFactory) {
|
|
100
|
+
invariant(descriptor && descriptor.Owner, 'IID owner is absent');
|
|
101
|
+
if (expectedOwner !== null) invariant(String(descriptor.Owner) === expectedOwner, 'IID owner changed');
|
|
102
|
+
invariant((descriptor.Control & SE_DACL_PRESENT) !== 0, 'IID DACL is absent');
|
|
103
|
+
invariant((descriptor.Control & SE_DACL_PROTECTED) !== 0, 'IID DACL is not protected');
|
|
104
|
+
var dacl = descriptor.DiscretionaryAcl;
|
|
105
|
+
invariant(dacl && dacl.AceCount === 1, 'IID DACL must contain one ACE');
|
|
106
|
+
var entries = enumeratorFactory ? enumeratorFactory(dacl) : new Enumerator(dacl);
|
|
107
|
+
invariant(!entries.atEnd(), 'IID ACE is absent');
|
|
108
|
+
var ace = entries.item();
|
|
109
|
+
invariant(ace.AceType === ACCESS_ALLOWED_ACE_TYPE, 'IID ACE must allow access');
|
|
110
|
+
invariant((ace.AccessMask >>> 0) === FILE_ALL_ACCESS, 'IID ACE is not full control');
|
|
111
|
+
invariant(ace.AceFlags === expectedAceFlags, 'IID ACE flags are not exact');
|
|
112
|
+
entries.moveNext();
|
|
113
|
+
invariant(entries.atEnd(), 'IID DACL contains extra ACEs');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rawDescriptor(utility, targetPath) {
|
|
117
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
|
|
118
|
+
return bytesFromSafeArray(utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_RAW));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function iidDescriptor(utility, targetPath) {
|
|
122
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
|
|
123
|
+
return utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_IID);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ownerSid(bytes) {
|
|
127
|
+
invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
|
|
128
|
+
invariant(bytes[0] === 1, 'unsupported security descriptor revision');
|
|
129
|
+
invariant((u16(bytes, 2) & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
|
|
130
|
+
return sidAt(bytes, u32(bytes, 4)).value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readTargets(environment, fso) {
|
|
134
|
+
var countText = String(environment('FORGE_PRIVATE_ACL_COUNT'));
|
|
135
|
+
invariant(/^[1-9][0-9]*$/.test(countText), 'invalid target count');
|
|
136
|
+
var count = Number(countText);
|
|
137
|
+
invariant(count <= MAX_TARGETS, 'too many ACL targets');
|
|
138
|
+
var targets = [];
|
|
139
|
+
for (var i = 0; i < count; i++) {
|
|
140
|
+
var targetPath = String(environment('FORGE_PRIVATE_ACL_TARGET_' + i));
|
|
141
|
+
invariant(targetPath.length > 0 && targetPath.indexOf('\0') === -1, 'invalid ACL target');
|
|
142
|
+
var isDirectory = fso.FolderExists(targetPath);
|
|
143
|
+
var isFile = fso.FileExists(targetPath);
|
|
144
|
+
invariant(isDirectory !== isFile, 'ACL target does not exist');
|
|
145
|
+
targets.push({ isDirectory: isDirectory, path: targetPath });
|
|
146
|
+
}
|
|
147
|
+
return targets;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function applyOwnerOnlyDacl(utility, target, currentSid, createObject, descriptor) {
|
|
151
|
+
var dacl = createObject('AccessControlList');
|
|
152
|
+
dacl.AclRevision = 2;
|
|
153
|
+
var ace = createObject('AccessControlEntry');
|
|
154
|
+
ace.Trustee = currentSid;
|
|
155
|
+
ace.AccessMask = FILE_ALL_ACCESS;
|
|
156
|
+
ace.AceType = ACCESS_ALLOWED_ACE_TYPE;
|
|
157
|
+
ace.AceFlags = target.isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
|
|
158
|
+
dacl.AddAce(ace);
|
|
159
|
+
descriptor.Owner = currentSid;
|
|
160
|
+
descriptor.DiscretionaryAcl = dacl;
|
|
161
|
+
descriptor.Control = descriptor.Control | SE_DACL_PRESENT | SE_DACL_PROTECTED;
|
|
162
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL | PROTECTED_DACL_SECURITY_INFORMATION;
|
|
163
|
+
utility.SetSecurityDescriptor(target.path, ADS_PATH_FILE, descriptor, ADS_SD_FORMAT_IID);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function runWithDependencies(dependencies) {
|
|
167
|
+
var environment = dependencies.environment;
|
|
168
|
+
var fso = dependencies.fso;
|
|
169
|
+
var utility = dependencies.utility;
|
|
170
|
+
var createObject = dependencies.createObject;
|
|
171
|
+
var enumeratorFactory = dependencies.enumeratorFactory;
|
|
172
|
+
var currentSid = String(environment('FORGE_PRIVATE_ACL_SID'));
|
|
173
|
+
invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(currentSid), 'invalid current SID');
|
|
174
|
+
var targets = readTargets(environment, fso);
|
|
175
|
+
var owners = [];
|
|
176
|
+
var iidOwners = [];
|
|
177
|
+
var i;
|
|
178
|
+
|
|
179
|
+
// Prove every target and descriptor are readable before the first mutation.
|
|
180
|
+
for (i = 0; i < targets.length; i++) {
|
|
181
|
+
var beforeRaw = atStage(20, function () { return rawDescriptor(utility, targets[i].path); });
|
|
182
|
+
var beforeIid = atStage(21, function () { return iidDescriptor(utility, targets[i].path); });
|
|
183
|
+
owners.push(atStage(22, function () { return ownerSid(beforeRaw); }));
|
|
184
|
+
iidOwners.push(atStage(23, function () {
|
|
185
|
+
invariant(beforeIid && beforeIid.Owner, 'precheck IID owner is absent');
|
|
186
|
+
return String(beforeIid.Owner);
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (i = 0; i < targets.length; i++) {
|
|
191
|
+
// Recheck immediately before changing the owner and DACL together.
|
|
192
|
+
var currentIid = atStage(30, function () {
|
|
193
|
+
var isDirectory = fso.FolderExists(targets[i].path);
|
|
194
|
+
var isFile = fso.FileExists(targets[i].path);
|
|
195
|
+
invariant(isDirectory === targets[i].isDirectory && isFile !== isDirectory, 'target type changed before mutation');
|
|
196
|
+
invariant(ownerSid(rawDescriptor(utility, targets[i].path)) === owners[i], 'owner changed before mutation');
|
|
197
|
+
var descriptor = iidDescriptor(utility, targets[i].path);
|
|
198
|
+
invariant(descriptor && descriptor.Owner, 'IID owner is absent before mutation');
|
|
199
|
+
invariant(String(descriptor.Owner) === iidOwners[i], 'IID owner changed before mutation');
|
|
200
|
+
return descriptor;
|
|
201
|
+
});
|
|
202
|
+
atStage(31, function () { applyOwnerOnlyDacl(utility, targets[i], currentSid, createObject, currentIid); });
|
|
203
|
+
var expectedFlags = targets[i].isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
|
|
204
|
+
atStage(32, function () {
|
|
205
|
+
inspectDescriptor(rawDescriptor(utility, targets[i].path), currentSid, expectedFlags);
|
|
206
|
+
});
|
|
207
|
+
atStage(33, function () {
|
|
208
|
+
inspectIidDescriptor(iidDescriptor(utility, targets[i].path), null, expectedFlags, enumeratorFactory);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function main() {
|
|
215
|
+
try {
|
|
216
|
+
var shell = new ActiveXObject('WScript.Shell');
|
|
217
|
+
runWithDependencies({
|
|
218
|
+
createObject: function (name) { return new ActiveXObject(name); },
|
|
219
|
+
environment: shell.Environment('PROCESS'),
|
|
220
|
+
fso: new ActiveXObject('Scripting.FileSystemObject'),
|
|
221
|
+
utility: new ActiveXObject('ADsSecurityUtility')
|
|
222
|
+
});
|
|
223
|
+
WScript.Quit(0);
|
|
224
|
+
} catch (_error) {
|
|
225
|
+
var exitCode = _error && typeof _error.forgeExitCode === 'number' ? _error.forgeExitCode : 1;
|
|
226
|
+
WScript.Quit(exitCode);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
231
|
+
module.exports = {
|
|
232
|
+
FILE_ALL_ACCESS: FILE_ALL_ACCESS,
|
|
233
|
+
inspectDescriptor: inspectDescriptor,
|
|
234
|
+
inspectIidDescriptor: inspectIidDescriptor,
|
|
235
|
+
runWithDependencies: runWithDependencies
|
|
236
|
+
};
|
|
237
|
+
} else {
|
|
238
|
+
main();
|
|
239
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const MEMORY_REVIEW_ENTRY_LIMIT = 200;
|
|
6
|
+
const DEFAULT_FINDING_LIMIT = 50;
|
|
7
|
+
const STALE_AFTER_DAYS = 90;
|
|
8
|
+
const STALE_AFTER_MS = STALE_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
|
9
|
+
const VOLATILE_FIELDS = new Set([
|
|
10
|
+
'id', 'key', 'memory_id', 'created_at', 'updated_at', 'timestamp', 'revision',
|
|
11
|
+
'createdAt', 'updatedAt',
|
|
12
|
+
]);
|
|
13
|
+
const NEGATION_PREFIXES = Object.freeze(['do not ', "don't ", 'never ']);
|
|
14
|
+
|
|
15
|
+
function compareCodeUnits(left, right) {
|
|
16
|
+
if (left < right) return -1;
|
|
17
|
+
if (left > right) return 1;
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeText(value) {
|
|
22
|
+
return String(value)
|
|
23
|
+
.normalize('NFKC')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[’']/g, "'")
|
|
26
|
+
.replace(/[^\p{L}\p{N}'\s]/gu, ' ')
|
|
27
|
+
.replace(/\s+/g, ' ')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canonicalize(value) {
|
|
32
|
+
if (typeof value === 'string') return normalizeText(value);
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map(canonicalize).sort((left, right) =>
|
|
35
|
+
compareCodeUnits(JSON.stringify(left), JSON.stringify(right)));
|
|
36
|
+
}
|
|
37
|
+
if (!value || typeof value !== 'object') return value;
|
|
38
|
+
return Object.fromEntries(Object.keys(value)
|
|
39
|
+
.filter(key => !VOLATILE_FIELDS.has(key))
|
|
40
|
+
.sort(compareCodeUnits)
|
|
41
|
+
.map(key => [key, canonicalize(value[key])]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function payloadFor(entry) {
|
|
45
|
+
if (entry && Object.hasOwn(entry, 'note')) return entry.note;
|
|
46
|
+
if (entry && Object.hasOwn(entry, 'value')) return entry.value;
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fingerprint(value) {
|
|
51
|
+
return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function reviewId(kind, parts) {
|
|
55
|
+
const digest = createHash('sha256')
|
|
56
|
+
.update(`${kind}\0${[...parts].sort(compareCodeUnits).join('\0')}`)
|
|
57
|
+
.digest('hex')
|
|
58
|
+
.slice(0, 16);
|
|
59
|
+
return `memory-${kind}-${digest}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function claimFor(entry) {
|
|
63
|
+
const payload = payloadFor(entry);
|
|
64
|
+
if (typeof payload !== 'string') return null;
|
|
65
|
+
let text = normalizeText(payload);
|
|
66
|
+
let negative = false;
|
|
67
|
+
const prefix = NEGATION_PREFIXES.find(candidate => text.startsWith(candidate));
|
|
68
|
+
if (prefix) {
|
|
69
|
+
negative = true;
|
|
70
|
+
text = text.slice(prefix.length).trim();
|
|
71
|
+
}
|
|
72
|
+
return text ? { basis: text, negative } : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readOwnDataProperty(value, fieldName, subject) {
|
|
76
|
+
let descriptor;
|
|
77
|
+
try {
|
|
78
|
+
descriptor = Object.getOwnPropertyDescriptor(value, fieldName);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new TypeError(`${subject} ${fieldName} must be an own data property`);
|
|
81
|
+
}
|
|
82
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
83
|
+
throw new TypeError(`${subject} ${fieldName} must be an own data property`);
|
|
84
|
+
}
|
|
85
|
+
return descriptor.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stalenessForMemory(memory, usageProjection, options = {}) {
|
|
89
|
+
if (!memory || typeof memory !== 'object') {
|
|
90
|
+
throw new TypeError('memory must be an object');
|
|
91
|
+
}
|
|
92
|
+
const assertCanonicalIso = (value, fieldName) => {
|
|
93
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))
|
|
94
|
+
|| new Date(value).toISOString() !== value) {
|
|
95
|
+
throw new TypeError(`${fieldName} must be a canonical ISO timestamp`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const createdAt = readOwnDataProperty(memory, 'created_at', 'memory');
|
|
99
|
+
assertCanonicalIso(createdAt, 'memory created_at');
|
|
100
|
+
let useCount = 0;
|
|
101
|
+
let baselineAt = createdAt;
|
|
102
|
+
if (usageProjection !== null && usageProjection !== undefined) {
|
|
103
|
+
if (!usageProjection || typeof usageProjection !== 'object') {
|
|
104
|
+
throw new TypeError('durable usage projection must be an object');
|
|
105
|
+
}
|
|
106
|
+
const lastUsedAt = readOwnDataProperty(usageProjection, 'last_used_at', 'durable usage projection');
|
|
107
|
+
const projectedUseCount = readOwnDataProperty(usageProjection, 'use_count', 'durable usage projection');
|
|
108
|
+
assertCanonicalIso(lastUsedAt, 'durable last_used_at');
|
|
109
|
+
if (!Number.isSafeInteger(projectedUseCount) || projectedUseCount < 0) {
|
|
110
|
+
throw new TypeError('durable usage projection use_count must be a nonnegative safe integer');
|
|
111
|
+
}
|
|
112
|
+
baselineAt = lastUsedAt;
|
|
113
|
+
useCount = projectedUseCount;
|
|
114
|
+
}
|
|
115
|
+
if (!options || typeof options !== 'object') {
|
|
116
|
+
throw new TypeError('staleness review options must be an object');
|
|
117
|
+
}
|
|
118
|
+
const now = readOwnDataProperty(options, 'now', 'staleness review options');
|
|
119
|
+
assertCanonicalIso(now, 'staleness review now');
|
|
120
|
+
const ageMs = Date.parse(now) - Date.parse(baselineAt);
|
|
121
|
+
const stale = ageMs > STALE_AFTER_MS;
|
|
122
|
+
return {
|
|
123
|
+
baseline_at: baselineAt,
|
|
124
|
+
use_count: useCount,
|
|
125
|
+
stale,
|
|
126
|
+
demote: stale,
|
|
127
|
+
age_days: Math.max(0, Math.floor(ageMs / (24 * 60 * 60 * 1000))),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function reviewMemories(entries, options = {}) {
|
|
132
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
133
|
+
const entryLimit = Math.max(1, Math.min(options.entryLimit || MEMORY_REVIEW_ENTRY_LIMIT, MEMORY_REVIEW_ENTRY_LIMIT));
|
|
134
|
+
const findingLimit = Math.max(1, Math.min(options.findingLimit || DEFAULT_FINDING_LIMIT, DEFAULT_FINDING_LIMIT));
|
|
135
|
+
const selected = source.slice(0, entryLimit);
|
|
136
|
+
const findings = [];
|
|
137
|
+
|
|
138
|
+
const duplicateGroups = new Map();
|
|
139
|
+
for (const entry of selected) {
|
|
140
|
+
const payload = payloadFor(entry);
|
|
141
|
+
const canonical = JSON.stringify(canonicalize(payload));
|
|
142
|
+
const group = duplicateGroups.get(canonical) || [];
|
|
143
|
+
group.push(fingerprint(payload));
|
|
144
|
+
duplicateGroups.set(canonical, group);
|
|
145
|
+
}
|
|
146
|
+
for (const [canonical, members] of duplicateGroups) {
|
|
147
|
+
if (members.length < 2) continue;
|
|
148
|
+
findings.push({
|
|
149
|
+
review_id: reviewId('duplicate', [canonical]),
|
|
150
|
+
kind: 'duplicate',
|
|
151
|
+
count: members.length,
|
|
152
|
+
members: [...new Set(members)].sort(compareCodeUnits),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const claims = new Map();
|
|
157
|
+
for (const entry of selected) {
|
|
158
|
+
const claim = claimFor(entry);
|
|
159
|
+
if (!claim) continue;
|
|
160
|
+
const sides = claims.get(claim.basis) || { positive: [], negative: [] };
|
|
161
|
+
sides[claim.negative ? 'negative' : 'positive'].push(fingerprint(payloadFor(entry)));
|
|
162
|
+
claims.set(claim.basis, sides);
|
|
163
|
+
}
|
|
164
|
+
for (const [basis, sides] of claims) {
|
|
165
|
+
if (!sides.positive.length || !sides.negative.length) continue;
|
|
166
|
+
findings.push({
|
|
167
|
+
review_id: reviewId('contradiction', [basis]),
|
|
168
|
+
kind: 'contradiction',
|
|
169
|
+
claim: basis,
|
|
170
|
+
members: [...new Set([...sides.positive, ...sides.negative])].sort(compareCodeUnits),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
findings.sort((left, right) => compareCodeUnits(left.review_id, right.review_id));
|
|
175
|
+
const total = Number.isInteger(options.total) && options.total >= source.length
|
|
176
|
+
? options.total
|
|
177
|
+
: source.length;
|
|
178
|
+
return {
|
|
179
|
+
total,
|
|
180
|
+
scanned: selected.length,
|
|
181
|
+
truncated: total > selected.length || findings.length > findingLimit,
|
|
182
|
+
findings: findings.slice(0, findingLimit),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
MEMORY_REVIEW_ENTRY_LIMIT,
|
|
188
|
+
STALE_AFTER_DAYS,
|
|
189
|
+
reviewMemories,
|
|
190
|
+
stalenessForMemory,
|
|
191
|
+
};
|
package/lib/memory/router.js
CHANGED
|
@@ -29,11 +29,16 @@ const crypto = require('node:crypto');
|
|
|
29
29
|
const fs = require('node:fs');
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const projectMemory = require('../project-memory');
|
|
32
|
+
const {
|
|
33
|
+
assertBackend,
|
|
34
|
+
createMemoryBackendRegistry,
|
|
35
|
+
} = require('@forge/memory');
|
|
32
36
|
|
|
33
37
|
/** Supported PUBLIC backends, default first. */
|
|
34
38
|
const MEMORY_BACKENDS = ['local', 'graphiti'];
|
|
35
39
|
const DEFAULT_MEMORY_BACKEND = 'local';
|
|
36
40
|
const ENV_VAR = 'FORGE_MEMORY_BACKEND';
|
|
41
|
+
const registeredMemoryBackends = new Map();
|
|
37
42
|
|
|
38
43
|
/** Default recall cap — newest-N, so `recall` with no query never dumps the whole store. */
|
|
39
44
|
const DEFAULT_RECALL_LIMIT = 20;
|
|
@@ -126,7 +131,7 @@ function resolveMemoryBackend({
|
|
|
126
131
|
if (!value) return DEFAULT_MEMORY_BACKEND;
|
|
127
132
|
|
|
128
133
|
const normalized = value.toLowerCase();
|
|
129
|
-
if (MEMORY_BACKENDS.includes(normalized)) return normalized;
|
|
134
|
+
if (MEMORY_BACKENDS.includes(normalized) || registeredMemoryBackends.has(normalized)) return normalized;
|
|
130
135
|
|
|
131
136
|
warn(
|
|
132
137
|
`Unknown memory backend "${value}" from ${source}; `
|
|
@@ -135,6 +140,22 @@ function resolveMemoryBackend({
|
|
|
135
140
|
return DEFAULT_MEMORY_BACKEND;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
function registerMemoryBackend(name, adapter) {
|
|
144
|
+
const normalized = typeof name === 'string' ? name.trim().toLowerCase() : '';
|
|
145
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(normalized) || MEMORY_BACKENDS.includes(normalized)) {
|
|
146
|
+
throw new TypeError('custom memory backend must have a unique lowercase name');
|
|
147
|
+
}
|
|
148
|
+
if (registeredMemoryBackends.has(normalized)) {
|
|
149
|
+
throw new Error(`custom memory backend "${normalized}" is already registered`);
|
|
150
|
+
}
|
|
151
|
+
assertBackend(adapter, normalized);
|
|
152
|
+
registeredMemoryBackends.set(normalized, adapter);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unregisterMemoryBackend(name) {
|
|
156
|
+
registeredMemoryBackends.delete(String(name || '').trim().toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
/**
|
|
139
160
|
* Strict validation for the resolved backend. Unlike `resolveMemoryBackend`
|
|
140
161
|
* (which soft-falls-back), this THROWS a clear, actionable error when the
|
|
@@ -145,6 +166,17 @@ function resolveMemoryBackend({
|
|
|
145
166
|
*/
|
|
146
167
|
function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, config } = {}) {
|
|
147
168
|
const memory = config || readMemoryConfig(projectRoot);
|
|
169
|
+
const signal = collectBackendSignal({ deps, env, projectRoot, config: memory });
|
|
170
|
+
const requested = signal.value?.toLowerCase();
|
|
171
|
+
if (requested && !MEMORY_BACKENDS.includes(requested) && !registeredMemoryBackends.has(requested)) {
|
|
172
|
+
const error = new Error(
|
|
173
|
+
`memory backend "${signal.value}" from ${signal.source} is not registered. `
|
|
174
|
+
+ `Available backends: ${[...MEMORY_BACKENDS, ...registeredMemoryBackends.keys()]
|
|
175
|
+
.sort((left, right) => left.localeCompare(right)).join(', ')}.`,
|
|
176
|
+
);
|
|
177
|
+
error.backend = signal.value;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
148
180
|
const backend = resolveMemoryBackend({ deps, env, projectRoot, config: memory, warn: () => {} });
|
|
149
181
|
|
|
150
182
|
if (backend !== 'graphiti') {
|
|
@@ -182,19 +214,32 @@ function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, co
|
|
|
182
214
|
* never-hang guarantee — the emitter, not this seam, owns preventing that.
|
|
183
215
|
*
|
|
184
216
|
* @param {object} [emitter] - optional `{ emit(entry) }` injected by callers.
|
|
185
|
-
* @param {
|
|
217
|
+
* @param {Function} entryProvider - returns the persisted local entry.
|
|
186
218
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
219
|
+
// The implementation now lives in the adapter below so synchronous failures can
|
|
220
|
+
// be represented in the operation receipt while rejected promises stay consumed.
|
|
221
|
+
|
|
222
|
+
function graphitiAdapter(emitter, entryProvider) {
|
|
223
|
+
const unavailable = () => [];
|
|
224
|
+
const freeze = (value, seen = new WeakSet()) => {
|
|
225
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return value;
|
|
226
|
+
seen.add(value);
|
|
227
|
+
for (const child of Object.values(value)) freeze(child, seen);
|
|
228
|
+
return Object.freeze(value);
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
add() {
|
|
232
|
+
if (!emitter || typeof emitter.emit !== 'function') return;
|
|
233
|
+
const maybePromise = emitter.emit(freeze(structuredClone(entryProvider())));
|
|
234
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
235
|
+
maybePromise.then(() => {}, () => {});
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
recall: unavailable,
|
|
239
|
+
search: unavailable,
|
|
240
|
+
capture: unavailable,
|
|
241
|
+
digest: unavailable,
|
|
242
|
+
};
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
/**
|
|
@@ -336,20 +381,38 @@ function migrateJsonlNotesOnce(projectRoot, options = {}) {
|
|
|
336
381
|
* @param {object} [options] - { tags, deps, env, config, graphitiEmitter, store }
|
|
337
382
|
* @returns {{ id: string, note: string, timestamp: string, tags: string[] }}
|
|
338
383
|
*/
|
|
339
|
-
function
|
|
384
|
+
function appendWithReceipt(projectRoot, note, options = {}) {
|
|
340
385
|
migrateJsonlNotesOnce(projectRoot, options);
|
|
341
386
|
const backend = resolveMemoryBackend({ ...options, projectRoot });
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
387
|
+
let entry;
|
|
388
|
+
const local = {
|
|
389
|
+
add() {
|
|
390
|
+
const written = projectMemory.write(projectRoot, {
|
|
391
|
+
key: crypto.randomUUID(),
|
|
392
|
+
value: note,
|
|
393
|
+
sourceAgent: REMEMBER_SOURCE_AGENT,
|
|
394
|
+
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
395
|
+
}, options);
|
|
396
|
+
entry = toNote(written);
|
|
397
|
+
return entry;
|
|
398
|
+
},
|
|
399
|
+
recall: () => [],
|
|
400
|
+
search: () => [],
|
|
401
|
+
capture: () => null,
|
|
402
|
+
digest: () => null,
|
|
403
|
+
};
|
|
404
|
+
const registry = createMemoryBackendRegistry({ local });
|
|
349
405
|
if (backend === 'graphiti') {
|
|
350
|
-
|
|
406
|
+
registry.register('graphiti', graphitiAdapter(options.graphitiEmitter, () => entry));
|
|
407
|
+
} else if (backend !== 'local') {
|
|
408
|
+
registry.register(backend, registeredMemoryBackends.get(backend));
|
|
351
409
|
}
|
|
352
|
-
|
|
410
|
+
registry.select(backend);
|
|
411
|
+
return registry.add(note);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function append(projectRoot, note, options = {}) {
|
|
415
|
+
return appendWithReceipt(projectRoot, note, options).value;
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
/**
|
|
@@ -363,7 +426,7 @@ function append(projectRoot, note, options = {}) {
|
|
|
363
426
|
* widens it to every stored memory.
|
|
364
427
|
*
|
|
365
428
|
* @param {string} projectRoot
|
|
366
|
-
* @param {object} [selection] - { query, limit, all }
|
|
429
|
+
* @param {object} [selection] - { query, limit, all, kind }
|
|
367
430
|
* @param {object} [options] - forwarded to the project-memory read paths (e.g. a store).
|
|
368
431
|
* @returns {{ notes: object[], total: number, capped: boolean, query: string, limit: number, scope: string }}
|
|
369
432
|
*/
|
|
@@ -373,15 +436,16 @@ function recall(projectRoot, selection = {}, options = {}) {
|
|
|
373
436
|
const limit = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_RECALL_LIMIT;
|
|
374
437
|
const query = String(selection.query || '').trim();
|
|
375
438
|
const includeAll = Boolean(selection.all);
|
|
439
|
+
const kind = typeof selection.kind === 'string' ? selection.kind.trim() : '';
|
|
440
|
+
const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
|
|
441
|
+
const readOptions = { ...options, agents, kind };
|
|
376
442
|
|
|
377
443
|
if (query) {
|
|
378
|
-
const notes = projectMemory.searchRanked(projectRoot, query, limit,
|
|
444
|
+
const notes = projectMemory.searchRanked(projectRoot, query, limit, readOptions).map(toNote);
|
|
379
445
|
// BM25 returns at most `limit`; a full result set signals there may be more.
|
|
380
446
|
return { notes, total: notes.length, capped: notes.length >= limit, query, limit, scope: 'all' };
|
|
381
447
|
}
|
|
382
448
|
|
|
383
|
-
const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
|
|
384
|
-
const readOptions = { ...options, agents };
|
|
385
449
|
const notes = projectMemory.recent(projectRoot, limit, readOptions).map(toNote);
|
|
386
450
|
const total = projectMemory.count(projectRoot, readOptions);
|
|
387
451
|
return { notes, total, capped: total > notes.length, query: '', limit, scope: includeAll ? 'all' : 'remembered' };
|
|
@@ -395,8 +459,11 @@ module.exports = {
|
|
|
395
459
|
readMemoryConfig,
|
|
396
460
|
resolveMemoryBackend,
|
|
397
461
|
assertMemoryConfigValid,
|
|
462
|
+
registerMemoryBackend,
|
|
463
|
+
unregisterMemoryBackend,
|
|
398
464
|
migrateJsonlNotesOnce,
|
|
399
465
|
toNote,
|
|
400
466
|
append,
|
|
467
|
+
appendWithReceipt,
|
|
401
468
|
recall,
|
|
402
469
|
};
|