import type { Diagnostic, ResolvedProfileContext, SemanticGraph, WorkspaceSource } from './compiler.js'; /** * The version of condition evaluation itself, not of the package. * * A report says which catalogue asked its questions. It could not say which * engine answered them, so a consumer holding stored answers could tell a * model change from a catalogue deepening (via `since`) but not from a change * in what a condition means. ADR 0097 replaced four aspect rules with the * ArchiMate 3.2 table and flipped `missing-relationship` answers for unchanged * models and unchanged questions; ADR 0083's `unconstrained-kind` goes * near-empty under that same table. Neither was visible in any report. * * **Bump this when an existing question's answer can change for an unchanged * model.** Do not bump it for anything else: not a release, not a new * condition, not a catalogue edit, not a rendering change. A version that * moves when answers did not is a version consumers learn to ignore. * * `test/interrogation-semantics.test.ts` fingerprints every condition against * a fixture and fails if evaluation moves without this bumping, so the rule is * enforced rather than remembered. */ export declare const INTERROGATION_SEMANTICS_VERSION = "2"; export interface CatalogueSelector { /** * Kinds to select. Absent selects every concept, which is what a * kind-agnostic condition wants: succession can be declared on any subject, * so enumerating the kinds that may carry it would be a list nobody can keep * right rather than a constraint (ADR 0109). */ readonly kinds?: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; readonly statuses?: readonly string[]; readonly documents?: readonly string[]; } export type CatalogueCondition = { readonly condition: 'missing-claim'; readonly predicate: string; } | { readonly condition: 'missing-relationship'; readonly kinds: readonly string[]; readonly direction: 'incoming' | 'outgoing' | 'any'; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'isolated'; } | { readonly condition: 'no-subject-of-kind'; readonly kinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { /** * The positive twin of `no-subject-of-kind`, and workspace-scope like it * (#398). A gate wants the opposite polarity from a question: a question * exists to be closed by an absence ending, while a gate opens once the * thing is there. `opensWhen` requires every condition to hold and has no * `not`, so inverting was not available and the phase-ordered interview * an adopter was authoring could say "the model still lacks X" but never * "the model now has X". * * ADR 0125 anticipated arrivals in this position: a further condition * "can join later without changing the mechanism". */ readonly condition: 'has-subject-of-kind'; readonly kinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { /** * The cardinality member of the same family, workspace-scope like the * other two (#411). Fires while FEWER than `atLeast` subjects of the * named kinds exist, so `no-subject-of-kind` is its `atLeast: 1` case. * * It exists because a **vocabulary question** — "which data sensitivity * classes does this platform recognise?" — had no way to say it wanted * more than one term. `no-subject-of-kind` closes on the first * instance, so a one-term vocabulary was indistinguishable from a * complete one, and an adopter measured two live cases where a class * authored incidentally to answer a different question closed the * vocabulary question before it was ever asked. * * `atLeast` is required and must be at least 2 (`YM918`): 1 is * `no-subject-of-kind` spelled a second way, and 0 is a condition that * can never fire, which is what `YM914` exists to refuse. * * Deliberately NOT a general numeric comparison. A kind's population * against a floor is the whole need, and the narrow condition is the * same trade that kept `has-subject-of-kind` a twin rather than a * predicate: a vocabulary of parameterised comparisons is a query * language, which this design declines. */ readonly condition: 'below-subject-count'; readonly kinds: readonly string[]; readonly atLeast: number; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'no-state-defined'; } | { readonly condition: 'missing-linkage'; readonly kinds: readonly string[]; readonly direction: 'incoming' | 'outgoing'; readonly counterpartKinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'has-linkage'; readonly kinds: readonly string[]; readonly direction: 'incoming' | 'outgoing' | 'either'; readonly counterpartKinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'exists-linkage'; readonly kinds: readonly string[]; readonly direction: 'incoming' | 'outgoing' | 'either'; readonly counterpartKinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { /** * The negative twin of `exists-linkage`, workspace-scope like it * (#436, ADR 0138). Fires while NO concept in the workspace satisfies * the linkage, so a question can ask "nothing anywhere links this way". * * It exists because a **vocabulary question** was asking the wrong * thing. `below-subject-count` measures a population, and a vocabulary * question means "did anyone survey this" — a proxy that fails in both * directions, measured on a live engagement: two throwaway values close * it dishonestly, while a truthful single-value estate can never close * it at all. * * `MODEL-FLOOR.md` prescribes the answer's home: a classification axis * is a `grouping` that aggregates its members, so a scheme aggregating * its classes IS the statement that these are the classes. Asking "no * scheme aggregates any class" needs this condition; asking how many * classes exist does not reach it. * * Deliberately NOT `!exists-linkage` in the evaluator, for the reason * `has-subject-of-kind` is not `!no-subject-of-kind`: written as its own * check, an empty workspace falls out right rather than by double * negative. */ readonly condition: 'no-linkage-exists'; readonly kinds: readonly string[]; readonly direction: 'incoming' | 'outgoing' | 'either'; readonly counterpartKinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'missing-constraint'; readonly kinds: readonly string[]; readonly kindMatching?: 'exact' | 'descendants'; } | { readonly condition: 'missing-flow-content'; } | { readonly condition: 'missing-reference'; readonly predicate: string; readonly direction: 'incoming' | 'outgoing'; } | { readonly condition: 'missing-attestation'; readonly topic: string; } | { readonly condition: 'near-duplicate'; } | { readonly condition: 'unconstrained-kind'; } | { readonly condition: 'unscoped-succession'; } | { readonly condition: 'unchallenged-evidence'; } | { readonly condition: 'has-any-subject'; } | { /** * The subject fills a slot of a pattern instance (ADR 0131). A GUARD, * per the #334 split: it says a question applies here, and an * ordinary condition beside it says what would answer it. Bare, it * means bound into any slot of any instance; `patternKinds` narrows * by the pattern's kind identity (never a document path, ADR 0129) * and `slots` by part name. */ readonly condition: 'fills-pattern-slot'; readonly patternKinds?: readonly string[]; readonly slots?: readonly string[]; } | { /** * The subject is a pattern INSTANCE with a slot nothing is bound into * (#447). The mirror of `fills-pattern-slot`, and the mechanism by which * a pattern becomes a questionnaire: bare, it means any slot of this * instance's pattern is unbound; `patternKinds` narrows by the pattern's * kind identity and `slots` by part name, exactly as its mirror does. * * It fires for a REQUIRED slot too, but only where one can survive a * compile: an instance that declares `parts` and omits a required one is * YM416 and there is no result to read, while an instance that declares * no `parts` at all never reaches YM416 and is exactly the greenfield * case ADR 0123 left open. The vacancy row's `required` tells the two * apart for the host; the condition itself does not read it, because a * catalogue that wants only one of them says so with `slots`. */ readonly condition: 'missing-part'; readonly patternKinds?: readonly string[]; readonly slots?: readonly string[]; }; /** * One observation from the workspace's evidence overlay, reduced to what * interrogation reads: the result, and whether a search was recorded with * it. The only condition that reads the overlay is `unchallenged-evidence`; * every other condition reads the compiled graph alone, and the overlay * never influences which subjects a selector matches. * * Shaped structurally rather than importing {@link EvidenceObservation} so * the pure engine entry (`./interrogation-entry`) keeps owning its whole * input surface: a caller passes * `evidenceDocuments.flatMap(({ observations }) => observations)` and the * wider evidence shape is never dragged in. */ export interface CatalogueEvidenceObservation { readonly result: 'confirmed' | 'contradicted' | 'unknown' | 'not-observed'; readonly searched?: readonly unknown[]; } /** * One slot of one pattern instance and the subject bound into it, as * interrogation reads it. Shaped structurally rather than importing the * compiler's {@link PatternMembership} for the same reason * {@link CatalogueEvidenceObservation} is: the pure engine entry keeps * owning its whole input surface, and a host passes * `compilation.patternMemberships` without the compiler's types. Only * `fills-pattern-slot` reads it (ADR 0131). */ export interface CataloguePatternMembership { readonly member: string; readonly slot: string; readonly instance: string; readonly pattern: string; } /** * One vacant optional slot of one pattern instance, as interrogation reads * it. Structural for the same reason {@link CataloguePatternMembership} is: * a host passes `compilation.patternVacancies` and the compiler's types are * never dragged into the pure engine entry. Only `missing-part` reads it. */ export interface CataloguePatternVacancy { readonly instance: string; readonly pattern: string; readonly slot: string; readonly slotKind: string; /** * The pattern declares this part required. The condition does NOT read it — * a vacancy is a vacancy — but it travels because the host derives its * answer shape from this row, and "you have not decided this yet" and "this * model does not stand up without it" are different questions to put to a * person (#447). */ readonly required: boolean; } export interface CatalogueQuestion { readonly id: string; readonly wave: string; readonly scope: 'workspace' | 'subject'; readonly subjects?: CatalogueSelector; readonly trigger: readonly CatalogueCondition[]; readonly question: string; readonly askPlain?: string; readonly materiality: string; readonly resolution: string; readonly authority: 'human' | 'agent' | 'either'; readonly since?: string; } export interface QuestionCatalogue { readonly format: 'yarramate/question-catalogue/v1'; readonly id: string; readonly version: string; readonly profile: string; readonly presentation?: { readonly title?: string; readonly description?: string; }; readonly waves: readonly { readonly id: string; readonly name: string; readonly description?: string; /** * Conditions that must all hold before this wave opens (#334, ADR 0125). * Absent means always open, which is what every wave did before this. */ readonly opensWhen?: readonly CatalogueCondition[]; }[]; readonly questions: readonly CatalogueQuestion[]; } export interface OpenSubject { readonly id: string; readonly name?: string; readonly question: string; } export interface ReportQuestion { readonly id: string; readonly scope: 'workspace' | 'subject'; readonly authority: 'human' | 'agent' | 'either'; readonly open: boolean; /** * Whether the question was asked at all (#375, ADR 0132). Absent means * true; only a subject-scoped question whose selector matched NO subject * carries `asked: false`. Without it, never-asked and answered were * byte-identical (`open: false`), and a host summing closed questions * read an empty model as a satisfied interview — completion inferred * from an empty set, the exact reading ADR 0125 refuses one level up by * not evaluating a closed wave's questions at all. */ readonly asked?: boolean; readonly question: string; readonly materiality: string; readonly resolution: string; /** * The catalogue trigger, verbatim (#289). The conditions that opened a * question are its machine-readable answer shape: a host builds the * matching affordance (a prefilled form, an operations skeleton) from * them instead of re-deriving the shape from its own catalogue copy and * drifting from engine semantics. */ readonly trigger: readonly CatalogueCondition[]; readonly since?: string; readonly subjects?: readonly OpenSubject[]; } export interface ReportWave { readonly id: string; readonly name: string; /** * Whether the wave's gate is met (#334, ADR 0125). A wave with no * `opensWhen` is always open. * * A wave reported `false` carries NO questions and contributes nothing to * the summary. Its questions are premature rather than answered, and a * progress rail that counted them as answered would flatter itself exactly * where someone is most likely to trust it. */ readonly opened: boolean; readonly questions: readonly ReportQuestion[]; } export interface InterrogationSummary { readonly questions: number; readonly openQuestions: number; readonly open: number; } export interface InterrogationReport { readonly format: 'yarramate/interrogation-report/v1'; readonly workspace: string; /** The BASE catalogue, `id@version`. Unchanged in shape by composition. */ readonly catalogue: string; /** * Every contributing catalogue as `id@version`, base first, when more than * one contributed (#345, ADR 0129). Optional so that adding it breaks no * constructor, and `catalogue` keeps its value shape so it breaks no reader. */ readonly catalogues?: readonly string[]; /** {@link INTERROGATION_SEMANTICS_VERSION} at the time of evaluation. */ readonly semantics: string; /** * Which optional inputs the evaluation was GIVEN (#450). A condition that * reads one it was not given stays quiet, which is right - the caller did * not look - but a quiet condition and a satisfied one are both * `open: false`, so without this a host summing closed questions reads * "nothing was supplied" as "nothing is missing". For an absence question * like `missing-part` the silent direction is "the interview is satisfied", * which stops an agent working rather than making it do redundant work. * * `asked: false` (#375, ADR 0132) says the same thing one level up, for a * selector that matched no subject. This is the level below it, for inputs. * It is a separate field rather than a third `asked` value because `asked` * is published and because "no subject" and "no data" are different facts a * host acts on differently. * * REQUIRED, on ADR 0110's reasoning for `trigger`: the fact exists for every * report, so an optional field would force every consumer to write an * absent-case branch for a case that cannot occur. Every key is present on * every report, so a host reads a boolean rather than testing for presence. * * To find which QUESTIONS could not be evaluated, join a question's echoed * `trigger` to this map through {@link conditionInput}. */ readonly inputs: Readonly>; readonly summary: InterrogationSummary; readonly waves: readonly ReportWave[]; } /** * What a condition needs in order to mean anything: a subject, or only the * workspace (#400). * * A wave gate evaluates with NO subject, and the catalogue schema offered the * whole vocabulary in that position, so a subject-scope condition in * `opensWhen` produced a wave that silently never opened (`has-linkage`, * `near-duplicate`, `fills-pattern-slot`) or a gate that was silently inert * (`missing-linkage`, `isolated`, `missing-claim`, `missing-constraint`) — * measured, both halves. Neither was refused. That is the same failure * `YM914` already refuses from a different cause: a gate nothing can satisfy * is indistinguishable from a gate that is merely unmet. * * This is a `Record` over the union's discriminant rather than a list of the * workspace-scope names, and that is the point. An allowlist cannot fail for * the author who wrote it (CONTRIBUTING.md's ninth rule), so a new condition * must not be able to arrive and be quietly absent from a gate check. Here it * cannot: adding a member to `CatalogueCondition` is a TYPECHECK ERROR until * its scope is declared, so the compiler asks the question rather than this * table remembering the answer. */ /** * An optional input to {@link evaluateCatalogue} that some condition needs in * order to mean anything (#450). * * `catalogues` is not one: it names contributing catalogues in the report and * no condition reads it, so withholding it cannot silence anything. */ export type CatalogueInput = 'profileContext' | 'evidence' | 'patternMemberships' | 'patternVacancies'; /** * The input this condition goes quiet without, if any (#450). Published beside * {@link conditionScope} so a host can join a report's echoed trigger to its * `inputs` and answer the question it actually has: which of the questions in * front of me could not be evaluated? */ export declare const conditionInput: (condition: CatalogueCondition) => CatalogueInput | undefined; export declare const conditionScope: (condition: CatalogueCondition) => 'workspace' | 'subject'; export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string; export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext, evidence?: readonly CatalogueEvidenceObservation[], /** * Contributing catalogues, from {@link composeCatalogues}. Composition * happens BEFORE evaluation and hands this function an ordinary catalogue, * so the only thing evaluation learns about composition is what to name in * the report. A fifth optional parameter rather than an options object, * because this signature is published and a consumer already calls it. */ catalogues?: readonly string[], /** * Pattern memberships from the compilation (ADR 0131) — pass * `compilation.patternMemberships`, or `fills-pattern-slot` conditions * never fire. A sixth optional parameter for the same reason * `catalogues` is a fifth: this signature is published and a consumer * already calls it. */ patternMemberships?: readonly CataloguePatternMembership[], /** * Pattern vacancies from the compilation (#447) - pass * `compilation.patternVacancies`, or `missing-part` conditions never fire. * A seventh optional parameter for the same reason the sixth is one. */ patternVacancies?: readonly CataloguePatternVacancy[]): Omit; export type CatalogueLoadResult = { readonly ok: true; readonly catalogue: QuestionCatalogue; } | { readonly ok: false; readonly diagnostics: readonly Diagnostic[]; }; /** * A catalogue is qualified on the way OUT, never in what an author writes. * * The authored schema keeps ids local (`^[a-z][a-z0-9-]*$`) and that does not * change; a consultant writes `regulator-signoff`, not * `consulting#regulator-signoff`. The engine qualifies when it composes, which * is the only moment two catalogues can be confused for each other. * * NO VERSION, and that is the decision rather than an omission (ADR 0129). * `core-enrichment` went 1.0 to 1.3 in a single day renaming nothing; a * versioned identity would have stranded every stored dismissal in every * adopter's database three times that day, for changes that removed no * question. Versioned identity is safe for things that are AUTHORED - a * document keeps naming the version it was written against and an author * updates it deliberately - and unsafe for things that are STORED, because a * row in someone's database has no author to update it. */ export declare const qualifiedQuestionId: (catalogueId: string, questionId: string) => string; export interface ComposedCatalogue { /** * The composed catalogue, ready for `evaluateCatalogue`. Structurally a * `QuestionCatalogue`, but its question ids are QUALIFIED, so it is a * composition result rather than something an author could have written and * must never be validated against the authored schema again. */ readonly catalogue: QuestionCatalogue; /** Every contributing catalogue as `id@version`, base first. */ readonly catalogues: readonly string[]; } export type CatalogueCompositionResult = { readonly ok: true; readonly composed: ComposedCatalogue; } | { readonly ok: false; readonly diagnostics: readonly Diagnostic[]; }; /** * Compose a base catalogue with the ones a workspace carries (#345, ADR 0129). * * ADDITIVE. `--catalogue` and `MountOptions.catalogue` replace the base; this * adds to it, which is what lets a consultant author a question at any point * in an engagement with no product release. * * A WAVE IS DECLARED EXACTLY ONCE across the resolved set, and any catalogue * may contribute questions to a wave it did not declare. That one rule settles * three questions composition would otherwise raise. Wave identity: a project * catalogue joins a declared wave rather than colliding with it. Ordering: * only a declaration places a wave, so the base's order is untouched and new * waves append. And the `opensWhen` precedence ADR 0125 made load-bearing does * not arise at all, because there is only ever one declarer to ask. * * QUALIFICATION IS A PROPERTY OF COMPOSITION, NOT OF EVALUATION. A caller * that hands `evaluateCatalogue` a catalogue directly still gets local ids, * exactly as before this existed. Route through here even for ONE catalogue - * which is what every CLI verb does - and ids are qualified from the start, so * they do not change later when a workspace first carries a question of its * own. That transition is the one thing an adopter keying stored judgments on * a question id must not experience, and composing unconditionally is how it * is avoided. * * ID COLLISIONS DISSOLVE rather than being resolved. Two catalogues may both * carry `outcome-missing`; qualified, they are two different questions. There * is no merge rule, no last-wins and no refusal, because there is nothing to * merge. */ export declare function composeCatalogues(sources: readonly WorkspaceSource[], profileContext?: ResolvedProfileContext): CatalogueCompositionResult; export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource, profileContext?: ResolvedProfileContext): CatalogueLoadResult; export declare function renderInterrogationReport(report: InterrogationReport): string;