@groundnuty/macf 0.2.36 → 0.2.37
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-info.json +2 -2
- package/dist/cli/claude-sh.d.ts +12 -10
- package/dist/cli/claude-sh.d.ts.map +1 -1
- package/dist/cli/claude-sh.js +13 -11
- package/dist/cli/claude-sh.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +10 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/monitor.d.ts +16 -0
- package/dist/cli/commands/monitor.d.ts.map +1 -0
- package/dist/cli/commands/monitor.js +96 -0
- package/dist/cli/commands/monitor.js.map +1 -0
- package/dist/cli/commands/propose.d.ts +21 -0
- package/dist/cli/commands/propose.d.ts.map +1 -0
- package/dist/cli/commands/propose.js +128 -0
- package/dist/cli/commands/propose.js.map +1 -0
- package/dist/cli/commands/rules-refresh.d.ts +1 -0
- package/dist/cli/commands/rules-refresh.d.ts.map +1 -1
- package/dist/cli/commands/rules-refresh.js +22 -1
- package/dist/cli/commands/rules-refresh.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +23 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/env-files-update.d.ts.map +1 -1
- package/dist/cli/env-files-update.js +5 -1
- package/dist/cli/env-files-update.js.map +1 -1
- package/dist/cli/env-files.d.ts +38 -13
- package/dist/cli/env-files.d.ts.map +1 -1
- package/dist/cli/env-files.js +73 -14
- package/dist/cli/env-files.js.map +1 -1
- package/dist/cli/index.js +109 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/monitor/digest.d.ts +89 -0
- package/dist/cli/monitor/digest.d.ts.map +1 -0
- package/dist/cli/monitor/digest.js +232 -0
- package/dist/cli/monitor/digest.js.map +1 -0
- package/dist/cli/monitor/github-reader.d.ts +38 -0
- package/dist/cli/monitor/github-reader.d.ts.map +1 -0
- package/dist/cli/monitor/github-reader.js +65 -0
- package/dist/cli/monitor/github-reader.js.map +1 -0
- package/dist/cli/monitor/reflections.d.ts +18 -0
- package/dist/cli/monitor/reflections.d.ts.map +1 -0
- package/dist/cli/monitor/reflections.js +72 -0
- package/dist/cli/monitor/reflections.js.map +1 -0
- package/dist/cli/monitor/run.d.ts +30 -0
- package/dist/cli/monitor/run.d.ts.map +1 -0
- package/dist/cli/monitor/run.js +67 -0
- package/dist/cli/monitor/run.js.map +1 -0
- package/dist/cli/project-rules.d.ts +105 -0
- package/dist/cli/project-rules.d.ts.map +1 -0
- package/dist/cli/project-rules.js +305 -0
- package/dist/cli/project-rules.js.map +1 -0
- package/dist/cli/propose/candidates.d.ts +95 -0
- package/dist/cli/propose/candidates.d.ts.map +1 -0
- package/dist/cli/propose/candidates.js +117 -0
- package/dist/cli/propose/candidates.js.map +1 -0
- package/dist/cli/propose/invariants.d.ts +49 -0
- package/dist/cli/propose/invariants.d.ts.map +1 -0
- package/dist/cli/propose/invariants.js +154 -0
- package/dist/cli/propose/invariants.js.map +1 -0
- package/dist/cli/propose/proposal-writer.d.ts +33 -0
- package/dist/cli/propose/proposal-writer.d.ts.map +1 -0
- package/dist/cli/propose/proposal-writer.js +53 -0
- package/dist/cli/propose/proposal-writer.js.map +1 -0
- package/dist/cli/propose/report.d.ts +49 -0
- package/dist/cli/propose/report.d.ts.map +1 -0
- package/dist/cli/propose/report.js +227 -0
- package/dist/cli/propose/report.js.map +1 -0
- package/dist/cli/propose/run.d.ts +41 -0
- package/dist/cli/propose/run.d.ts.map +1 -0
- package/dist/cli/propose/run.js +62 -0
- package/dist/cli/propose/run.js.map +1 -0
- package/dist/cli/settings-writer.d.ts +76 -6
- package/dist/cli/settings-writer.d.ts.map +1 -1
- package/dist/cli/settings-writer.js +115 -6
- package/dist/cli/settings-writer.js.map +1 -1
- package/dist/reconciler/reconcile.d.ts +31 -0
- package/dist/reconciler/reconcile.d.ts.map +1 -1
- package/dist/reconciler/reconcile.js +47 -3
- package/dist/reconciler/reconcile.js.map +1 -1
- package/dist/reconciler/run.d.ts +21 -1
- package/dist/reconciler/run.d.ts.map +1 -1
- package/dist/reconciler/run.js +106 -17
- package/dist/reconciler/run.js.map +1 -1
- package/package.json +2 -2
- package/plugin/rules/gh-token-attribution-traps.md +4 -0
- package/plugin/rules/observability-wiring.md +3 -3
- package/plugin/rules/reflection-staging.md +65 -0
- package/plugin/rules/silent-fallback-hazards.md +21 -4
- package/scripts/check-auditor-never-acts.sh +167 -0
- package/scripts/check-gh-attribution.sh +230 -0
- package/scripts/emit-turn-receipt.sh +1 -1
- package/scripts/harvest-reflection.sh +125 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Monitor orchestrator for the auditor (groundnuty/macf#502, DR-026 F4).
|
|
3
|
+
*
|
|
4
|
+
* Aggregates a project's coordination substrate — GitHub (open issues + open
|
|
5
|
+
* PRs, via an injectable read-only seam), F2 reflection ledgers, and a defensive
|
|
6
|
+
* project-tier rule count — and renders a protocol-health digest via the pure
|
|
7
|
+
* `buildDigest`.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is a READ. The GitHub seam is `GitHubReader` (list-only by
|
|
10
|
+
* type); reflections + project-rule counts are filesystem reads. The clock is
|
|
11
|
+
* injected so output is deterministic under test.
|
|
12
|
+
*/
|
|
13
|
+
import { readdirSync, existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { buildDigest } from './digest.js';
|
|
16
|
+
import { readReflections } from './reflections.js';
|
|
17
|
+
/**
|
|
18
|
+
* Count `.claude/rules/project/*.md` files. Defensive: returns 0 (never throws)
|
|
19
|
+
* when the directory is absent — F3 isn't merged, so its absence is normal.
|
|
20
|
+
*/
|
|
21
|
+
export function countProjectRules(workspaceDir) {
|
|
22
|
+
const dir = join(workspaceDir, '.claude', 'rules', 'project');
|
|
23
|
+
if (!existsSync(dir))
|
|
24
|
+
return 0;
|
|
25
|
+
try {
|
|
26
|
+
return readdirSync(dir).filter((f) => {
|
|
27
|
+
if (!f.endsWith('.md'))
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return statSync(join(dir, f)).isFile();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}).length;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run the Monitor: read everything, build the digest, return the Markdown.
|
|
43
|
+
*
|
|
44
|
+
* Only READS are performed. Any GitHub read failure is surfaced as a thrown
|
|
45
|
+
* error to the caller (the CLI command), which decides how to report it.
|
|
46
|
+
*/
|
|
47
|
+
export async function runMonitor(opts) {
|
|
48
|
+
const [issues, prs] = await Promise.all([
|
|
49
|
+
opts.reader.listOpenIssues(opts.repo),
|
|
50
|
+
opts.reader.listOpenPRs(opts.repo),
|
|
51
|
+
]);
|
|
52
|
+
const reflections = readReflections(opts.reflectionsDir);
|
|
53
|
+
const projectRulesCount = countProjectRules(opts.workspaceDir);
|
|
54
|
+
return buildDigest({
|
|
55
|
+
project: opts.project,
|
|
56
|
+
repo: opts.repo,
|
|
57
|
+
sinceDays: opts.sinceDays,
|
|
58
|
+
issues,
|
|
59
|
+
prs,
|
|
60
|
+
reflections: reflections.records,
|
|
61
|
+
reflectionsSkipped: reflections.skipped,
|
|
62
|
+
reflectionFiles: reflections.files,
|
|
63
|
+
projectRulesCount,
|
|
64
|
+
now: opts.now,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/cli/monitor/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAc,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkBnD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;YACnC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;YACrC,IAAI,CAAC;gBACH,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC,CAAC,CAAC,MAAM,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAuB;IACtD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;KACnC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACzD,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAE/D,OAAO,WAAW,CAAC;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM;QACN,GAAG;QACH,WAAW,EAAE,WAAW,CAAC,OAAO;QAChC,kBAAkB,EAAE,WAAW,CAAC,OAAO;QACvC,eAAe,EAAE,WAAW,CAAC,KAAK;QAClC,iBAAiB;QACjB,GAAG,EAAE,IAAI,CAAC,GAAG;KACd,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Env var carrying the project-rules source (operator-managed config). */
|
|
2
|
+
export declare const PROJECT_RULES_SOURCE_ENV = "MACF_PROJECT_RULES_SOURCE";
|
|
3
|
+
/**
|
|
4
|
+
* Destination directory for project-tier rules within a workspace:
|
|
5
|
+
* `<workspace>/.claude/rules/project/`. A SUBDIR of the universal-rule dir so
|
|
6
|
+
* the two tiers are distinguishable on disk.
|
|
7
|
+
*/
|
|
8
|
+
export declare function workspaceProjectRulesDir(workspaceDir: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Parsed shape of a `MACF_PROJECT_RULES_SOURCE` value.
|
|
11
|
+
*
|
|
12
|
+
* - `kind: 'github'` — `<owner>/<repo>//<path>` form: fetched via git clone.
|
|
13
|
+
* - `kind: 'local'` — a local directory path: copied directly.
|
|
14
|
+
*/
|
|
15
|
+
export type ProjectRulesSource = {
|
|
16
|
+
readonly kind: 'github';
|
|
17
|
+
readonly owner: string;
|
|
18
|
+
readonly repo: string;
|
|
19
|
+
readonly subpath: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'local';
|
|
22
|
+
readonly dir: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse a `MACF_PROJECT_RULES_SOURCE` value into a discriminated source.
|
|
26
|
+
*
|
|
27
|
+
* The `//` separator disambiguates the two forms unambiguously: a GitHub
|
|
28
|
+
* `<owner>/<repo>//<path>` always contains `//`, a local filesystem path
|
|
29
|
+
* never does (`//` collapses to `/` on POSIX and isn't a meaningful path
|
|
30
|
+
* token). So presence of `//` selects the github form; absence selects local.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` for an empty/whitespace-only value (caller treats as
|
|
33
|
+
* no-op) or a malformed github form (missing owner/repo/path segment) —
|
|
34
|
+
* malformed is surfaced by the caller as a warning, not a parse crash.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseProjectRulesSource(raw: string | undefined): ProjectRulesSource | undefined;
|
|
37
|
+
export interface FetchProjectRulesOptions {
|
|
38
|
+
/** Override the env value (defaults to process.env[MACF_PROJECT_RULES_SOURCE]). */
|
|
39
|
+
readonly source?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Override the GitHub base URL for the github-source form (tests point this
|
|
42
|
+
* at a local bare repo). Defaults to `https://github.com`.
|
|
43
|
+
*/
|
|
44
|
+
readonly githubBaseUrl?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Distribute project-tier rules into `<workspace>/.claude/rules/project/`.
|
|
48
|
+
*
|
|
49
|
+
* Resolves the source from `MACF_PROJECT_RULES_SOURCE` (or `options.source`):
|
|
50
|
+
* - Unset/empty/whitespace → NO-OP, returns `[]`. The tier is optional.
|
|
51
|
+
* - `<owner>/<repo>//<path>` → shallow git clone the default branch (mirrors
|
|
52
|
+
* plugin-fetcher.ts), copy `<clone>/<path>/*.md` to the dest subdir.
|
|
53
|
+
* - a local directory path → copy `<dir>/*.md` directly.
|
|
54
|
+
*
|
|
55
|
+
* Only `*.md` files are copied (a `.example` template, a README, etc. in the
|
|
56
|
+
* source are ignored — same filter as the universal-rule copy). Each copied
|
|
57
|
+
* file gets the PROJECT_RULE_HEADER prepended (unless it already opens with an
|
|
58
|
+
* HTML comment, to avoid double-stacking).
|
|
59
|
+
*
|
|
60
|
+
* **Idempotent + non-destructive to other tiers.** The dest is a fresh subdir;
|
|
61
|
+
* the universal `.claude/rules/*.md` files are never touched (different path).
|
|
62
|
+
* Re-running overwrites the project subdir's `*.md` files with the current
|
|
63
|
+
* source content. A `.example` seed (from `macf init`) is preserved — only
|
|
64
|
+
* `*.md` files are managed here.
|
|
65
|
+
*
|
|
66
|
+
* Throws only on a genuine fetch failure (git clone error, source path is a
|
|
67
|
+
* file not a directory, the github subpath is missing in the clone). A
|
|
68
|
+
* malformed source string is a no-op-with-warning, NOT a throw, so a typo'd
|
|
69
|
+
* config can't break `macf update`.
|
|
70
|
+
*
|
|
71
|
+
* @returns the list of copied basenames (empty when the source is unset or
|
|
72
|
+
* the source dir has no `*.md` files).
|
|
73
|
+
*/
|
|
74
|
+
export declare function fetchProjectRules(workspaceDir: string, options?: FetchProjectRulesOptions): readonly string[];
|
|
75
|
+
/**
|
|
76
|
+
* Basename of the seed template `macf init` drops into the project-rules
|
|
77
|
+
* subdir. The `.example` suffix keeps it OUT of the live-rule set (only `*.md`
|
|
78
|
+
* are loaded as rules + managed by `fetchProjectRules`), so it self-documents
|
|
79
|
+
* the tier + gives the DR-026 §4 auditor a format to match without becoming a
|
|
80
|
+
* rule itself.
|
|
81
|
+
*/
|
|
82
|
+
export declare const PROJECT_RULE_SEED_NAME = "EXAMPLE.project-rule.md.example";
|
|
83
|
+
/**
|
|
84
|
+
* Generic, deployment-agnostic seed content for the project-rules tier.
|
|
85
|
+
*
|
|
86
|
+
* CRITICAL: this ships via `macf init` to EVERY deployment, so it must NOT be
|
|
87
|
+
* macf-specific. A genomics deployment must not receive a macf `dev.mk` rule.
|
|
88
|
+
* The body demonstrates the FORMAT + the precedence contract with neutral
|
|
89
|
+
* placeholders only — never a concrete macf rule.
|
|
90
|
+
*/
|
|
91
|
+
export declare function projectRuleSeedContent(): string;
|
|
92
|
+
/**
|
|
93
|
+
* Seed the project-rules subdir with the generic `.example` template
|
|
94
|
+
* (`macf init`). Creates `.claude/rules/project/` (mkdir -p) and writes the
|
|
95
|
+
* `.example` file. Idempotent: overwrites the `.example` with the current
|
|
96
|
+
* canonical template — it's a managed example, not operator state. Returns the
|
|
97
|
+
* seeded basename.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately does NOT fetch from `MACF_PROJECT_RULES_SOURCE` — `macf init`
|
|
100
|
+
* only lays down the tier + its self-documenting seed; `macf update` (and
|
|
101
|
+
* `macf rules refresh`) pull the actual rules from the source. This keeps init
|
|
102
|
+
* generic (it ships to every deployment) and offline-safe.
|
|
103
|
+
*/
|
|
104
|
+
export declare function seedProjectRulesDir(workspaceDir: string): string;
|
|
105
|
+
//# sourceMappingURL=project-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rules.d.ts","sourceRoot":"","sources":["../../src/cli/project-rules.ts"],"names":[],"mappings":"AAiDA,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,8BAA8B,CAAC;AAsBpE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpG;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,CAmB/F;AAED,MAAM,WAAW,wBAAwB;IACvC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,wBAA6B,GACrC,SAAS,MAAM,EAAE,CA0DnB;AAwBD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,oCAAoC,CAAC;AAExE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA8D/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAKhE"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-tier rule distribution (groundnuty/macf#501, DR-026 F3).
|
|
3
|
+
*
|
|
4
|
+
* The three-tier rule model (DR-026 §3) splits coordination knowledge into:
|
|
5
|
+
* 1. Universal / product rules — shipped IN the CLI at `plugin/rules/*.md`,
|
|
6
|
+
* copied to `.claude/rules/*.md` (see rules.ts `copyCanonicalRules`).
|
|
7
|
+
* 2. **Project rules** — per-deployment, NOT shipped in the npm product. This
|
|
8
|
+
* module. Sourced from an explicit config and copied to a SUBDIR
|
|
9
|
+
* `.claude/rules/project/*.md` so the universal and project tiers are
|
|
10
|
+
* distinguishable on disk (DR-026 §4's subordination check needs a clean
|
|
11
|
+
* target to match against).
|
|
12
|
+
* 3. Agent-private rules — an agent's own `.claude/rules/` / memory; out of
|
|
13
|
+
* scope here.
|
|
14
|
+
*
|
|
15
|
+
* **Why explicit config, not "the coordination repo".** A multi-repo deployment
|
|
16
|
+
* has no single repo to implicitly pull project rules from, so the source MUST
|
|
17
|
+
* be configured. `MACF_PROJECT_RULES_SOURCE` carries it, in one of two forms:
|
|
18
|
+
*
|
|
19
|
+
* - `<owner>/<repo>//<path>` — a GitHub repo + subdir (e.g.
|
|
20
|
+
* `groundnuty/macf//project-rules`). Fetched via
|
|
21
|
+
* a shallow `git clone` of the default branch,
|
|
22
|
+
* mirroring plugin-fetcher.ts (reuse, don't
|
|
23
|
+
* reinvent). The `//` separates repo from path.
|
|
24
|
+
* - a local directory path — anything WITHOUT the `//` separator that
|
|
25
|
+
* resolves to a directory on disk. Copied
|
|
26
|
+
* directly (no clone). Lets a single-host
|
|
27
|
+
* deployment point at a checked-out repo or a
|
|
28
|
+
* plain rules directory.
|
|
29
|
+
*
|
|
30
|
+
* **Optional tier — never errors on absence.** When the env var is unset/empty,
|
|
31
|
+
* `fetchProjectRules` is a no-op returning `[]`. The tier is optional, exactly
|
|
32
|
+
* like a workspace that has no universal rules: absence is normal, not a fault.
|
|
33
|
+
*
|
|
34
|
+
* **No shadow of the universal tier.** Project rules land under
|
|
35
|
+
* `.claude/rules/project/`, a different subdir from the universal
|
|
36
|
+
* `.claude/rules/*.md`. The universal-rule copy path (rules.ts) is untouched,
|
|
37
|
+
* so this can never overwrite a universal rule.
|
|
38
|
+
*
|
|
39
|
+
* **Precedence (documented, NOT enforced here).** Project rules *add to /
|
|
40
|
+
* specialize* the universal protocol; they never weaken it. The §4
|
|
41
|
+
* invariant-check that enforces "never contradict" is a separate (gated) slice
|
|
42
|
+
* of DR-026 — F3 only lays down the on-disk tiers. See
|
|
43
|
+
* `design/project-tier-rules.md`.
|
|
44
|
+
*/
|
|
45
|
+
import { execFileSync } from 'node:child_process';
|
|
46
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
47
|
+
import { tmpdir } from 'node:os';
|
|
48
|
+
import { join, resolve } from 'node:path';
|
|
49
|
+
/** Env var carrying the project-rules source (operator-managed config). */
|
|
50
|
+
export const PROJECT_RULES_SOURCE_ENV = 'MACF_PROJECT_RULES_SOURCE';
|
|
51
|
+
/**
|
|
52
|
+
* Header prepended to each copied project rule, marking the on-disk tier +
|
|
53
|
+
* the managed-from-source contract. Distinct wording from the universal-rule
|
|
54
|
+
* MANAGED_HEADER (rules.ts) so an operator inspecting a file knows which tier
|
|
55
|
+
* it belongs to + where it came from.
|
|
56
|
+
*/
|
|
57
|
+
const PROJECT_RULE_HEADER = [
|
|
58
|
+
'<!--',
|
|
59
|
+
' PROJECT-TIER RULE (DR-026 §3 tier 2). Distributed by `macf` from this',
|
|
60
|
+
` deployment's project-rules source (\`${PROJECT_RULES_SOURCE_ENV}\`). Do not`,
|
|
61
|
+
' edit this workspace copy — edits are overwritten on the next `macf update`.',
|
|
62
|
+
' Edit the rule at the source, then re-run `macf update` here.',
|
|
63
|
+
'',
|
|
64
|
+
' Project rules ADD TO / SPECIALIZE the universal rules in',
|
|
65
|
+
' .claude/rules/*.md; they must never contradict or weaken them',
|
|
66
|
+
' (DR-026 §4). See design/project-tier-rules.md.',
|
|
67
|
+
'-->',
|
|
68
|
+
'',
|
|
69
|
+
].join('\n');
|
|
70
|
+
/**
|
|
71
|
+
* Destination directory for project-tier rules within a workspace:
|
|
72
|
+
* `<workspace>/.claude/rules/project/`. A SUBDIR of the universal-rule dir so
|
|
73
|
+
* the two tiers are distinguishable on disk.
|
|
74
|
+
*/
|
|
75
|
+
export function workspaceProjectRulesDir(workspaceDir) {
|
|
76
|
+
return join(resolve(workspaceDir), '.claude', 'rules', 'project');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse a `MACF_PROJECT_RULES_SOURCE` value into a discriminated source.
|
|
80
|
+
*
|
|
81
|
+
* The `//` separator disambiguates the two forms unambiguously: a GitHub
|
|
82
|
+
* `<owner>/<repo>//<path>` always contains `//`, a local filesystem path
|
|
83
|
+
* never does (`//` collapses to `/` on POSIX and isn't a meaningful path
|
|
84
|
+
* token). So presence of `//` selects the github form; absence selects local.
|
|
85
|
+
*
|
|
86
|
+
* Returns `undefined` for an empty/whitespace-only value (caller treats as
|
|
87
|
+
* no-op) or a malformed github form (missing owner/repo/path segment) —
|
|
88
|
+
* malformed is surfaced by the caller as a warning, not a parse crash.
|
|
89
|
+
*/
|
|
90
|
+
export function parseProjectRulesSource(raw) {
|
|
91
|
+
const value = (raw ?? '').trim();
|
|
92
|
+
if (value === '')
|
|
93
|
+
return undefined;
|
|
94
|
+
const sepIdx = value.indexOf('//');
|
|
95
|
+
if (sepIdx < 0) {
|
|
96
|
+
// No `//` → local directory path.
|
|
97
|
+
return { kind: 'local', dir: value };
|
|
98
|
+
}
|
|
99
|
+
const repoPart = value.slice(0, sepIdx);
|
|
100
|
+
const subpath = value.slice(sepIdx + 2).replace(/^\/+|\/+$/g, '');
|
|
101
|
+
const ownerRepo = repoPart.split('/');
|
|
102
|
+
if (ownerRepo.length !== 2 || !ownerRepo[0] || !ownerRepo[1] || subpath === '') {
|
|
103
|
+
// Malformed github form (e.g. "owner//path" with no repo, or trailing
|
|
104
|
+
// empty subpath). Don't throw — caller warns + skips.
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return { kind: 'github', owner: ownerRepo[0], repo: ownerRepo[1], subpath };
|
|
108
|
+
}
|
|
109
|
+
const DEFAULT_GITHUB_BASE_URL = 'https://github.com';
|
|
110
|
+
/**
|
|
111
|
+
* Distribute project-tier rules into `<workspace>/.claude/rules/project/`.
|
|
112
|
+
*
|
|
113
|
+
* Resolves the source from `MACF_PROJECT_RULES_SOURCE` (or `options.source`):
|
|
114
|
+
* - Unset/empty/whitespace → NO-OP, returns `[]`. The tier is optional.
|
|
115
|
+
* - `<owner>/<repo>//<path>` → shallow git clone the default branch (mirrors
|
|
116
|
+
* plugin-fetcher.ts), copy `<clone>/<path>/*.md` to the dest subdir.
|
|
117
|
+
* - a local directory path → copy `<dir>/*.md` directly.
|
|
118
|
+
*
|
|
119
|
+
* Only `*.md` files are copied (a `.example` template, a README, etc. in the
|
|
120
|
+
* source are ignored — same filter as the universal-rule copy). Each copied
|
|
121
|
+
* file gets the PROJECT_RULE_HEADER prepended (unless it already opens with an
|
|
122
|
+
* HTML comment, to avoid double-stacking).
|
|
123
|
+
*
|
|
124
|
+
* **Idempotent + non-destructive to other tiers.** The dest is a fresh subdir;
|
|
125
|
+
* the universal `.claude/rules/*.md` files are never touched (different path).
|
|
126
|
+
* Re-running overwrites the project subdir's `*.md` files with the current
|
|
127
|
+
* source content. A `.example` seed (from `macf init`) is preserved — only
|
|
128
|
+
* `*.md` files are managed here.
|
|
129
|
+
*
|
|
130
|
+
* Throws only on a genuine fetch failure (git clone error, source path is a
|
|
131
|
+
* file not a directory, the github subpath is missing in the clone). A
|
|
132
|
+
* malformed source string is a no-op-with-warning, NOT a throw, so a typo'd
|
|
133
|
+
* config can't break `macf update`.
|
|
134
|
+
*
|
|
135
|
+
* @returns the list of copied basenames (empty when the source is unset or
|
|
136
|
+
* the source dir has no `*.md` files).
|
|
137
|
+
*/
|
|
138
|
+
export function fetchProjectRules(workspaceDir, options = {}) {
|
|
139
|
+
const raw = options.source ?? process.env[PROJECT_RULES_SOURCE_ENV];
|
|
140
|
+
const parsed = parseProjectRulesSource(raw);
|
|
141
|
+
if (parsed === undefined) {
|
|
142
|
+
if ((raw ?? '').trim() !== '') {
|
|
143
|
+
// Non-empty but unparseable — surface it; don't silently drop.
|
|
144
|
+
process.stderr.write(`Warning: ${PROJECT_RULES_SOURCE_ENV}="${raw}" is malformed.\n` +
|
|
145
|
+
` Expected "<owner>/<repo>//<path>" (e.g. groundnuty/macf//project-rules)\n` +
|
|
146
|
+
` or a local directory path. Skipping project-rule distribution.\n`);
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
if (parsed.kind === 'local') {
|
|
151
|
+
const srcDir = resolve(parsed.dir);
|
|
152
|
+
if (!existsSync(srcDir)) {
|
|
153
|
+
throw new Error(`${PROJECT_RULES_SOURCE_ENV} local source does not exist: ${srcDir}`);
|
|
154
|
+
}
|
|
155
|
+
if (!statSync(srcDir).isDirectory()) {
|
|
156
|
+
throw new Error(`${PROJECT_RULES_SOURCE_ENV} local source is not a directory: ${srcDir}`);
|
|
157
|
+
}
|
|
158
|
+
return copyMarkdownRules(srcDir, workspaceProjectRulesDir(workspaceDir));
|
|
159
|
+
}
|
|
160
|
+
// github form — shallow-clone the default branch (mirrors plugin-fetcher.ts),
|
|
161
|
+
// copy the subpath's *.md files, discard the clone.
|
|
162
|
+
const baseUrl = options.githubBaseUrl ?? DEFAULT_GITHUB_BASE_URL;
|
|
163
|
+
const repoUrl = `${baseUrl}/${parsed.owner}/${parsed.repo}`;
|
|
164
|
+
const tmpClone = mkdtempSync(join(tmpdir(), 'macf-project-rules-clone-'));
|
|
165
|
+
try {
|
|
166
|
+
execFileSync('git', ['clone', '--depth', '1', repoUrl, tmpClone], {
|
|
167
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
const srcDir = join(tmpClone, parsed.subpath);
|
|
170
|
+
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
171
|
+
throw new Error(`Project-rules subpath "${parsed.subpath}" not found in ${repoUrl}. ` +
|
|
172
|
+
`Check ${PROJECT_RULES_SOURCE_ENV} and the repo layout.`);
|
|
173
|
+
}
|
|
174
|
+
return copyMarkdownRules(srcDir, workspaceProjectRulesDir(workspaceDir));
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
if (err instanceof Error && err.message.includes('subpath'))
|
|
178
|
+
throw err;
|
|
179
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
180
|
+
throw new Error(`Failed to fetch project rules from ${repoUrl}: ${msg}. ` +
|
|
181
|
+
`Check network access + that ${PROJECT_RULES_SOURCE_ENV} is correct.`, { cause: err });
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
rmSync(tmpClone, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Copy every `*.md` file from `srcDir` to `destDir`, prepending the
|
|
189
|
+
* project-rule header. Creates `destDir` (mkdir -p). Returns copied basenames.
|
|
190
|
+
* Shared by both the local and github source paths.
|
|
191
|
+
*/
|
|
192
|
+
function copyMarkdownRules(srcDir, destDir) {
|
|
193
|
+
mkdirSync(destDir, { recursive: true });
|
|
194
|
+
const copied = [];
|
|
195
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
196
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
197
|
+
continue;
|
|
198
|
+
const content = readFileSync(join(srcDir, entry.name), 'utf-8');
|
|
199
|
+
const out = content.startsWith('<!--') ? content : PROJECT_RULE_HEADER + content;
|
|
200
|
+
writeFileSync(join(destDir, entry.name), out);
|
|
201
|
+
copied.push(entry.name);
|
|
202
|
+
}
|
|
203
|
+
return copied;
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Seed (.example) template — written by `macf init`
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
/**
|
|
209
|
+
* Basename of the seed template `macf init` drops into the project-rules
|
|
210
|
+
* subdir. The `.example` suffix keeps it OUT of the live-rule set (only `*.md`
|
|
211
|
+
* are loaded as rules + managed by `fetchProjectRules`), so it self-documents
|
|
212
|
+
* the tier + gives the DR-026 §4 auditor a format to match without becoming a
|
|
213
|
+
* rule itself.
|
|
214
|
+
*/
|
|
215
|
+
export const PROJECT_RULE_SEED_NAME = 'EXAMPLE.project-rule.md.example';
|
|
216
|
+
/**
|
|
217
|
+
* Generic, deployment-agnostic seed content for the project-rules tier.
|
|
218
|
+
*
|
|
219
|
+
* CRITICAL: this ships via `macf init` to EVERY deployment, so it must NOT be
|
|
220
|
+
* macf-specific. A genomics deployment must not receive a macf `dev.mk` rule.
|
|
221
|
+
* The body demonstrates the FORMAT + the precedence contract with neutral
|
|
222
|
+
* placeholders only — never a concrete macf rule.
|
|
223
|
+
*/
|
|
224
|
+
export function projectRuleSeedContent() {
|
|
225
|
+
return [
|
|
226
|
+
'<!--',
|
|
227
|
+
' EXAMPLE project-tier rule (template, not active).',
|
|
228
|
+
'',
|
|
229
|
+
' This file ends in `.example`, so `macf` does NOT load it as a live rule',
|
|
230
|
+
' and does NOT overwrite it on `macf update` (only `*.md` files in this',
|
|
231
|
+
' directory are managed). It self-documents the project-rules tier and',
|
|
232
|
+
' gives the DR-026 §4 auditor a format to match.',
|
|
233
|
+
'-->',
|
|
234
|
+
'',
|
|
235
|
+
'# Project-tier rules (`.claude/rules/project/`)',
|
|
236
|
+
'',
|
|
237
|
+
'This directory holds **project-tier** coordination rules (DR-026 §3, tier 2):',
|
|
238
|
+
'per-deployment rules that ADD TO or SPECIALIZE the universal rules in the',
|
|
239
|
+
'parent `.claude/rules/*.md` directory.',
|
|
240
|
+
'',
|
|
241
|
+
'## Where these come from',
|
|
242
|
+
'',
|
|
243
|
+
'Project rules are distributed by `macf` from this deployment\'s configured',
|
|
244
|
+
'source, set via the `MACF_PROJECT_RULES_SOURCE` operator config in',
|
|
245
|
+
'`.claude/.macf/env.project-rules`. The source is one of:',
|
|
246
|
+
'',
|
|
247
|
+
'- `<owner>/<repo>//<path>` — a GitHub repo + subdir',
|
|
248
|
+
' (e.g. `your-org/your-coordination-repo//project-rules`)',
|
|
249
|
+
'- a local directory path — for single-host deployments',
|
|
250
|
+
'',
|
|
251
|
+
'When `MACF_PROJECT_RULES_SOURCE` is unset, this tier is empty (it is',
|
|
252
|
+
'optional). Authored rules live at the SOURCE, not in this workspace copy —',
|
|
253
|
+
'edit them there, then run `macf update` here.',
|
|
254
|
+
'',
|
|
255
|
+
'## Precedence (the one hard contract)',
|
|
256
|
+
'',
|
|
257
|
+
'Project rules may **add to / specialize** the universal protocol but must',
|
|
258
|
+
'**never contradict or weaken** its protected invariants (e.g. reporter-owns',
|
|
259
|
+
'closure, the identity↔attribution guarantee, the no-self-merge / LGTM gate).',
|
|
260
|
+
'See `design/project-tier-rules.md` for the full precedence model.',
|
|
261
|
+
'',
|
|
262
|
+
'## Format to follow (replace this example)',
|
|
263
|
+
'',
|
|
264
|
+
'A project rule is a Markdown file. Name it `*.md` (this `.example` is a',
|
|
265
|
+
'template, not a rule). Suggested shape — adapt to your deployment:',
|
|
266
|
+
'',
|
|
267
|
+
'```markdown',
|
|
268
|
+
'# <Short rule title>',
|
|
269
|
+
'',
|
|
270
|
+
'## Applies to',
|
|
271
|
+
'',
|
|
272
|
+
'<Which agents / repos / situations this project rule governs.>',
|
|
273
|
+
'',
|
|
274
|
+
'## Rule',
|
|
275
|
+
'',
|
|
276
|
+
'<The specialization. State what it ADDS to the universal protocol — never',
|
|
277
|
+
'what it weakens. If it looks like it relaxes a universal invariant, it is',
|
|
278
|
+
'wrong by construction (DR-026 §4).>',
|
|
279
|
+
'',
|
|
280
|
+
'## Rationale',
|
|
281
|
+
'',
|
|
282
|
+
'<Why this deployment needs it. Link the incident / pattern it codifies.>',
|
|
283
|
+
'```',
|
|
284
|
+
'',
|
|
285
|
+
].join('\n');
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Seed the project-rules subdir with the generic `.example` template
|
|
289
|
+
* (`macf init`). Creates `.claude/rules/project/` (mkdir -p) and writes the
|
|
290
|
+
* `.example` file. Idempotent: overwrites the `.example` with the current
|
|
291
|
+
* canonical template — it's a managed example, not operator state. Returns the
|
|
292
|
+
* seeded basename.
|
|
293
|
+
*
|
|
294
|
+
* Deliberately does NOT fetch from `MACF_PROJECT_RULES_SOURCE` — `macf init`
|
|
295
|
+
* only lays down the tier + its self-documenting seed; `macf update` (and
|
|
296
|
+
* `macf rules refresh`) pull the actual rules from the source. This keeps init
|
|
297
|
+
* generic (it ships to every deployment) and offline-safe.
|
|
298
|
+
*/
|
|
299
|
+
export function seedProjectRulesDir(workspaceDir) {
|
|
300
|
+
const destDir = workspaceProjectRulesDir(workspaceDir);
|
|
301
|
+
mkdirSync(destDir, { recursive: true });
|
|
302
|
+
writeFileSync(join(destDir, PROJECT_RULE_SEED_NAME), projectRuleSeedContent());
|
|
303
|
+
return PROJECT_RULE_SEED_NAME;
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=project-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rules.js","sourceRoot":"","sources":["../../src/cli/project-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure candidate-proposal pipeline for the auditor Plan membrane
|
|
3
|
+
* (groundnuty/macf#503, DR-026 G1). This is the Analyze→Plan half of the
|
|
4
|
+
* MAPE-K loop: turn F4's reflection signals into RATIFIABLE proposals.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is deterministic CODE scaffolding. The LLM judgment (is this a
|
|
7
|
+
* real rule? is the text precise? is the tier right?) is NOT encoded — the code
|
|
8
|
+
* assembles the AGENT-AUTHORED signal content (`signal`/`proposed_tier`/
|
|
9
|
+
* `rationale` from `rule_evolution_signals`) into structured proposals and
|
|
10
|
+
* applies three LOAD-BEARING, mechanical safety gates:
|
|
11
|
+
*
|
|
12
|
+
* GATE 1 — N>1 = distinct AGENTS, not occurrences. A candidate is promotable
|
|
13
|
+
* only if it recurs across ≥ threshold DISTINCT `agent.name`s. Five hits from
|
|
14
|
+
* ONE agent is N=1 → HELD (reflection ≠ verification). We count the set of
|
|
15
|
+
* distinct agent names, never raw occurrences.
|
|
16
|
+
*
|
|
17
|
+
* GATE 3 — no-auto-drop on invariant-touch. Step 4 SURFACES which protected
|
|
18
|
+
* invariants a candidate touches + flags apparent-relaxations HIGH-RISK, but
|
|
19
|
+
* NEVER drops/rejects in code. The amendment clause lets the auditor PROPOSE
|
|
20
|
+
* an operator-ratified amendment, so auto-dropping would foreclose that path.
|
|
21
|
+
* (GATE 2 — dry-run-by-default — lives in the orchestrator/command + writer
|
|
22
|
+
* seam, not here.)
|
|
23
|
+
*
|
|
24
|
+
* The tier-router (step 3) groups by the `proposed_tier` HINT and marks
|
|
25
|
+
* `universal` candidates NEEDS-CONFIRMATION (never auto-route a universal
|
|
26
|
+
* promotion — it affects every deployment). The hint is a hint, not a decision.
|
|
27
|
+
*/
|
|
28
|
+
import type { ReflectionRecord } from '@groundnuty/macf-core';
|
|
29
|
+
import { type InvariantTouch, type ProtectedInvariant } from './invariants.js';
|
|
30
|
+
/** Default distinct-agent threshold for promotion (configurable). */
|
|
31
|
+
export declare const DEFAULT_MIN_AGENTS = 2;
|
|
32
|
+
/**
|
|
33
|
+
* The router routing decision for a candidate, derived from its `proposed_tier`
|
|
34
|
+
* HINT. `needs-confirmation` is the universal/canonical case (never auto-route);
|
|
35
|
+
* `project-draft` is the local-project-rule case; `review` is anything else (an
|
|
36
|
+
* unrecognised tier hint still surfaces, routed for plain operator review).
|
|
37
|
+
*/
|
|
38
|
+
export type RouteDecision = 'needs-confirmation' | 'project-draft' | 'review';
|
|
39
|
+
/** A fully-assembled, ratifiable candidate proposal (a survivor of GATE 1). */
|
|
40
|
+
export interface ProposalCandidate {
|
|
41
|
+
/** Dedup handle: the signal `key` when present, else the signal text. */
|
|
42
|
+
readonly handle: string;
|
|
43
|
+
/** Whether the grouping used an explicit `key` (vs falling back to text). */
|
|
44
|
+
readonly hasKey: boolean;
|
|
45
|
+
/** The agent-authored proposed tier HINT (verbatim). */
|
|
46
|
+
readonly proposedTier: string;
|
|
47
|
+
/** Representative agent-authored signal text. */
|
|
48
|
+
readonly signal: string;
|
|
49
|
+
/** All distinct agent-authored rationales contributing this candidate. */
|
|
50
|
+
readonly rationales: readonly string[];
|
|
51
|
+
/** Distinct agent names that corroborated this candidate (GATE 1 unit). */
|
|
52
|
+
readonly corroboratingAgents: readonly string[];
|
|
53
|
+
/** Distinct-agent count == corroboratingAgents.length (the GATE-1 number). */
|
|
54
|
+
readonly distinctAgents: number;
|
|
55
|
+
/** Raw occurrence count (records carrying this signal) — informational only. */
|
|
56
|
+
readonly occurrences: number;
|
|
57
|
+
/** Router decision from the tier hint. */
|
|
58
|
+
readonly route: RouteDecision;
|
|
59
|
+
/** Protected invariants this candidate plausibly touches (SURFACED, GATE 3). */
|
|
60
|
+
readonly invariantTouches: readonly InvariantTouch[];
|
|
61
|
+
/** True when the text reads like a relaxation → HIGH-RISK flag (GATE 3). */
|
|
62
|
+
readonly highRisk: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A candidate HELD below the distinct-agent threshold (reported, not dropped). */
|
|
65
|
+
export interface HeldCandidate {
|
|
66
|
+
readonly handle: string;
|
|
67
|
+
readonly hasKey: boolean;
|
|
68
|
+
readonly proposedTier: string;
|
|
69
|
+
readonly signal: string;
|
|
70
|
+
readonly distinctAgents: number;
|
|
71
|
+
readonly occurrences: number;
|
|
72
|
+
/** Why it was held — always the N<threshold reason for v1. */
|
|
73
|
+
readonly reason: string;
|
|
74
|
+
}
|
|
75
|
+
/** The full pipeline output: survivors + held + the threshold used. */
|
|
76
|
+
export interface CandidateSet {
|
|
77
|
+
readonly minAgents: number;
|
|
78
|
+
readonly promoted: readonly ProposalCandidate[];
|
|
79
|
+
readonly held: readonly HeldCandidate[];
|
|
80
|
+
}
|
|
81
|
+
/** Route a candidate from its tier hint (universal/canonical never auto-route). */
|
|
82
|
+
export declare function routeForTier(proposedTier: string): RouteDecision;
|
|
83
|
+
/**
|
|
84
|
+
* Run the full candidate pipeline over the reflection records.
|
|
85
|
+
*
|
|
86
|
+
* Steps (DR-026 G1 §The command):
|
|
87
|
+
* 1. Aggregate signals (distinct-agent grouping).
|
|
88
|
+
* 2. GATE 1 — keep candidates with distinctAgents ≥ minAgents; the rest go to
|
|
89
|
+
* `held` (visible, never silently dropped).
|
|
90
|
+
* 3. Tier-router — universal/canonical → needs-confirmation; project → draft.
|
|
91
|
+
* 4. Subordination-check — surface touched invariants + HIGH-RISK relaxation
|
|
92
|
+
* flag. NEVER drops (GATE 3).
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildCandidates(records: readonly ReflectionRecord[], invariants: readonly ProtectedInvariant[], minAgents?: number): CandidateSet;
|
|
95
|
+
//# sourceMappingURL=candidates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidates.d.ts","sourceRoot":"","sources":["../../../src/cli/propose/candidates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAuB,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,iBAAiB,CAAC;AAEzB,qEAAqE;AACrE,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAKpC;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,eAAe,GAAG,QAAQ,CAAC;AAE9E,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,EAAE,SAAS,cAAc,EAAE,CAAC;IACrD,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;CACzC;AA6DD,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,CAKhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,SAAS,GAAE,MAA2B,GACrC,YAAY,CA2Dd"}
|